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
@@ -5473,6 +5473,9 @@ const ClientInfo_SDK = /* @__PURE__ */proto3.makeEnum("livekit.ClientInfo.SDK",
5473
5473
  }, {
5474
5474
  no: 14,
5475
5475
  name: "ESP32"
5476
+ }, {
5477
+ no: 15,
5478
+ name: "DOTNET"
5476
5479
  }]);
5477
5480
  const ClientInfo_Capability = /* @__PURE__ */proto3.makeEnum("livekit.ClientInfo.Capability", [{
5478
5481
  no: 0,
@@ -5841,6 +5844,9 @@ const EncodingOptionsPreset = /* @__PURE__ */proto3.makeEnum("livekit.EncodingOp
5841
5844
  }, {
5842
5845
  no: 7,
5843
5846
  name: "PORTRAIT_H264_1080P_60"
5847
+ }, {
5848
+ no: 8,
5849
+ name: "PASSTHROUGH"
5844
5850
  }]);
5845
5851
  const EncodedFileType = /* @__PURE__ */proto3.makeEnum("livekit.EncodedFileType", [{
5846
5852
  no: 0,
@@ -7825,6 +7831,11 @@ const SyncState = /* @__PURE__ */proto3.makeMessageType("livekit.SyncState", ()
7825
7831
  kind: "message",
7826
7832
  T: PublishDataTrackResponse,
7827
7833
  repeated: true
7834
+ }, {
7835
+ no: 9,
7836
+ name: "data_subscription",
7837
+ kind: "message",
7838
+ T: UpdateDataSubscription
7828
7839
  }]);
7829
7840
  const DataChannelReceiveState = /* @__PURE__ */proto3.makeMessageType("livekit.DataChannelReceiveState", () => [{
7830
7841
  no: 1,
@@ -8697,6 +8708,11 @@ function onWorkerLogLevelChanged(cb) {
8697
8708
  workerLogLevelListeners.delete(cb);
8698
8709
  };
8699
8710
  }const maxRetryDelay = 7000;
8711
+ /**
8712
+ * Upper bound of the random delay added to the first retry, whose base delay is 0. It keeps a
8713
+ * brief network blip recovering quickly while spreading out clients that disconnect together.
8714
+ */
8715
+ const maxFirstRetryJitterInMs = 500;
8700
8716
  const DEFAULT_RETRY_DELAYS_IN_MS = [0, 300, 2 * 2 * 300, 3 * 3 * 300, 4 * 4 * 300, maxRetryDelay, maxRetryDelay, maxRetryDelay, maxRetryDelay, maxRetryDelay];
8701
8717
  class DefaultReconnectPolicy {
8702
8718
  constructor(retryDelays) {
@@ -8705,8 +8721,13 @@ class DefaultReconnectPolicy {
8705
8721
  nextRetryDelayInMs(context) {
8706
8722
  if (context.retryCount >= this._retryDelays.length) return null;
8707
8723
  const retryDelay = this._retryDelays[context.retryCount];
8708
- if (context.retryCount <= 1) return retryDelay;
8709
- return retryDelay + Math.random() * 1000;
8724
+ // The first retry has no base delay to scale, so it gets a fixed window instead. A zero at a
8725
+ // later index is an explicit request for an immediate retry and passes through the formula as zero.
8726
+ if (context.retryCount === 0 && retryDelay === 0) {
8727
+ return Math.random() * maxFirstRetryJitterInMs;
8728
+ }
8729
+ // Jitter scales with the delay (+/-50%) so clients spread out further on later retries.
8730
+ return Math.round(retryDelay * (0.5 + Math.random()));
8710
8731
  }
8711
8732
  }/******************************************************************************
8712
8733
  Copyright (c) Microsoft Corporation.
@@ -12279,7 +12300,7 @@ function getMatch(exp, ua) {
12279
12300
  }
12280
12301
  function getOSVersion(ua) {
12281
12302
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
12282
- }var version$1 = "2.22.3";const version = version$1;
12303
+ }var version$1 = "2.23.0";const version = version$1;
12283
12304
  const protocolVersion = 17;
12284
12305
  /** Initial client protocol. */
12285
12306
  const CLIENT_PROTOCOL_DEFAULT = 0;
@@ -12360,6 +12381,32 @@ class ConnectionError extends LivekitReasonedError {
12360
12381
  return new ConnectionError(message, ConnectionErrorReason.ServiceNotFound, undefined, serviceName);
12361
12382
  }
12362
12383
  }
12384
+ /**
12385
+ * Whether a failed connection attempt may be retried against a different LiveKit Cloud region.
12386
+ *
12387
+ * LiveKit Cloud signals project-level region pinning by returning 403 on the RTC paths when the
12388
+ * project is not allowed in the region the client geo-routed to. `/settings/regions` is
12389
+ * deliberately left reachable so the client can discover its allowed regions and connect there, so
12390
+ * a 403 must not be treated as terminal.
12391
+ *
12392
+ * A 401 stays terminal: no other region will accept a token this one rejected. So does the 404
12393
+ * "requested room does not exist" case, which is also reported as `NotAllowed`.
12394
+ *
12395
+ * We key on the status rather than the server's error message because that message is an
12396
+ * unversioned human-readable string; matching it would let a copy edit break already-shipped
12397
+ * clients. If a 403 really was a permissions failure rather than region pinning, every region
12398
+ * attempt fails the same way and the original error still surfaces — at the cost of one extra
12399
+ * region lookup.
12400
+ */
12401
+ function canFailOverToAnotherRegion(error) {
12402
+ if (error.reason === ConnectionErrorReason.Cancelled) {
12403
+ return false;
12404
+ }
12405
+ if (error.reason === ConnectionErrorReason.NotAllowed) {
12406
+ return error.status === 403;
12407
+ }
12408
+ return true;
12409
+ }
12363
12410
  class DeviceUnsupportedError extends LivekitError {
12364
12411
  constructor(message) {
12365
12412
  super(21, message !== null && message !== void 0 ? message : 'device is unsupported');
@@ -12693,7 +12740,7 @@ var RoomEvent;
12693
12740
  * Data packets provides the ability to use LiveKit to send/receive arbitrary payloads.
12694
12741
  * All participants in the room will receive the messages sent to the room.
12695
12742
  *
12696
- * args: (payload: NonSharedUint8Array, participant: [[Participant]], kind: [[DataPacket_Kind]], topic?: string)
12743
+ * args: (payload: NonSharedUint8Array, participant: [[Participant]] | undefined, kind: [[DataPacket_Kind]], topic?: string, encryptionType?: Encryption_Type, participantIdentity?: string)
12697
12744
  */
12698
12745
  RoomEvent["DataReceived"] = "dataReceived";
12699
12746
  /**
@@ -13051,6 +13098,7 @@ var EngineEvent;
13051
13098
  EngineEvent["Joined"] = "joined";
13052
13099
  EngineEvent["TokenRefreshed"] = "tokenRefreshed";
13053
13100
  EngineEvent["ServerRegionsReported"] = "serverRegionsReported";
13101
+ EngineEvent["RequestSubscribedCodecRefresh"] = "requestSubscribedCodecRefresh";
13054
13102
  })(EngineEvent || (EngineEvent = {}));
13055
13103
  var TrackEvent;
13056
13104
  (function (TrackEvent) {
@@ -13520,20 +13568,44 @@ function detectSilence(track_1) {
13520
13568
  let timeOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
13521
13569
  return function* () {
13522
13570
  const ctx = getNewAudioContext();
13523
- if (ctx) {
13524
- const analyser = ctx.createAnalyser();
13571
+ if (!ctx) {
13572
+ return false;
13573
+ }
13574
+ let source;
13575
+ let analyser;
13576
+ try {
13577
+ analyser = ctx.createAnalyser();
13525
13578
  analyser.fftSize = 2048;
13526
13579
  const bufferLength = analyser.frequencyBinCount;
13527
13580
  const dataArray = new Uint8Array(bufferLength);
13528
- const source = ctx.createMediaStreamSource(new MediaStream([track.mediaStreamTrack]));
13581
+ source = ctx.createMediaStreamSource(new MediaStream([track.mediaStreamTrack]));
13529
13582
  source.connect(analyser);
13583
+ // read through a function so that `state` isn't narrowed to its value from before `resume`
13584
+ const isRunning = () => ctx.state === 'running';
13585
+ if (!isRunning()) {
13586
+ try {
13587
+ // `resume` can stay pending indefinitely on a blocked page rather than rejecting, which
13588
+ // would keep the context and its nodes alive past the `finally` below
13589
+ yield Promise.race([ctx.resume(), sleep(200)]);
13590
+ } catch (_a) {
13591
+ // autoplay policies can keep us from resuming the context
13592
+ }
13593
+ if (!isRunning()) {
13594
+ // a context that isn't running doesn't process any audio, so the analyser would only ever
13595
+ // read silence. Report the track as not silent rather than raising a false positive.
13596
+ return false;
13597
+ }
13598
+ }
13530
13599
  yield sleep(timeOffset);
13531
13600
  analyser.getByteTimeDomainData(dataArray);
13532
13601
  const someNoise = dataArray.some(sample => sample !== 128 && sample !== 0);
13533
- ctx.close();
13534
13602
  return !someNoise;
13603
+ } finally {
13604
+ source === null || source === void 0 ? void 0 : source.disconnect();
13605
+ analyser === null || analyser === void 0 ? void 0 : analyser.disconnect();
13606
+ // don't let a failing teardown mask the detection result
13607
+ yield ctx.close().catch(() => {});
13535
13608
  }
13536
- return false;
13537
13609
  }();
13538
13610
  });
13539
13611
  }
@@ -13745,6 +13817,85 @@ function areDimensionsSmaller(a, b) {
13745
13817
  function layerDimensionsFor(trackInfo, quality) {
13746
13818
  var _a;
13747
13819
  return (_a = trackInfo.layers) === null || _a === void 0 ? void 0 : _a.find(l => l.quality === quality);
13820
+ }
13821
+ /**
13822
+ * Waits until `track` has actually produced a video frame.
13823
+ *
13824
+ * iOS keeps reporting the camera's sensor frame from `getSettings()` until the camera has
13825
+ * delivered its first picture, so a portrait capture initially reads as landscape. The arrival
13826
+ * of that first frame is the only reliable signal that the reported settings describe reality.
13827
+ *
13828
+ * Best effort, never throws. Resolves `true` when a frame was observed, `false` when no frame
13829
+ * can be observed (the track is already ended, muted or disabled) or `timeoutMs` elapsed first.
13830
+ *
13831
+ * @internal
13832
+ */
13833
+ function waitForFirstVideoFrame(track_1, timeoutMs_1) {
13834
+ return __awaiter(this, arguments, void 0, function (track, timeoutMs) {
13835
+ let candidateElements = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
13836
+ return function* () {
13837
+ var _a;
13838
+ if (typeof document === 'undefined') {
13839
+ return false;
13840
+ }
13841
+ if (track.readyState === 'ended' || track.muted || !track.enabled) {
13842
+ // no frame is ever going to arrive, don't hold the caller up for the full timeout
13843
+ return false;
13844
+ }
13845
+ // requestVideoFrameCallback (Safari 15.4 and up) is a platform capability, so the answer is the
13846
+ // same for every element
13847
+ const supportsFrameCallback = typeof HTMLVideoElement.prototype.requestVideoFrameCallback === 'function';
13848
+ // prefer an element that is already rendering this track (a local preview, typically) over
13849
+ // spinning up a second decode just to observe a frame
13850
+ const existingElement = candidateElements.find(el => el instanceof HTMLVideoElement && el.srcObject instanceof MediaStream && el.srcObject.getVideoTracks().includes(track) && (
13851
+ // without requestVideoFrameCallback we depend on `loadeddata`, which is one-shot: an element
13852
+ // that already decoded a frame (readyState >= HAVE_CURRENT_DATA) never fires it again.
13853
+ // Leave those alone and set up our own element, which starts out empty.
13854
+ supportsFrameCallback || el.readyState < 2));
13855
+ const element = existingElement !== null && existingElement !== void 0 ? existingElement : document.createElement('video');
13856
+ if (!existingElement) {
13857
+ element.playsInline = true;
13858
+ element.muted = true;
13859
+ element.srcObject = new MediaStream([track]);
13860
+ // the element is deliberately never added to the DOM, it only exists to pull frames.
13861
+ // WebKit delivers frames to a paused element too, so a rejected play() (Safari's low power
13862
+ // mode, for instance) is not a reason to stop waiting
13863
+ element.play().catch(error => {
13864
+ livekitLogger.debug('could not play element while waiting for the first video frame', {
13865
+ error
13866
+ });
13867
+ });
13868
+ }
13869
+ let timer;
13870
+ let frameHandle;
13871
+ let onLoadedData;
13872
+ try {
13873
+ return yield new Promise(resolve => {
13874
+ timer = setTimeout(() => resolve(false), timeoutMs);
13875
+ if (supportsFrameCallback) {
13876
+ frameHandle = element.requestVideoFrameCallback(() => resolve(true));
13877
+ } else {
13878
+ // Safari below 15.4 has no requestVideoFrameCallback. `loadeddata` fires once the first
13879
+ // frame has been decoded, which is the same signal a few milliseconds earlier.
13880
+ onLoadedData = () => resolve(true);
13881
+ element.addEventListener('loadeddata', onLoadedData);
13882
+ }
13883
+ });
13884
+ } finally {
13885
+ clearTimeout(timer);
13886
+ if (frameHandle !== undefined) {
13887
+ (_a = element.cancelVideoFrameCallback) === null || _a === void 0 ? void 0 : _a.call(element, frameHandle);
13888
+ }
13889
+ if (onLoadedData) {
13890
+ element.removeEventListener('loadeddata', onLoadedData);
13891
+ }
13892
+ if (!existingElement) {
13893
+ element.pause();
13894
+ element.srcObject = null;
13895
+ }
13896
+ }
13897
+ }();
13898
+ });
13748
13899
  }const BACKGROUND_REACTION_DELAY = 5000;
13749
13900
  // keep old audio elements when detached, we would re-use them since on iOS
13750
13901
  // Safari tracks which audio elements have been "blessed" by the user.
@@ -14384,6 +14535,32 @@ function isSafariBased() {
14384
14535
  const b = getBrowser();
14385
14536
  return (b === null || b === void 0 ? void 0 : b.name) === 'Safari' || (b === null || b === void 0 ? void 0 : b.os) === 'iOS';
14386
14537
  }
14538
+ /**
14539
+ * iPadOS has sent the desktop Macintosh user agent by default since iPadOS 13, so `getBrowser()`
14540
+ * reports `os: 'macOS'` there and every `os === 'iOS'` check misses it. macOS Safari implements no
14541
+ * touch events at all (`TouchEvent` undefined, `maxTouchPoints` 0) regardless of any touchscreen
14542
+ * attached, so touch capability separates the two. Measured on an iPad 6 / iPadOS 17
14543
+ * (`maxTouchPoints` 5) against macOS Safari 26.4 (`maxTouchPoints` 0).
14544
+ *
14545
+ * Deliberately a separate helper rather than a fix in `browserParser`: iPadOS spoofs the Mac
14546
+ * version too, so reporting `os: 'iOS'` there would leave `osVersion` at `10.15.7` and silently
14547
+ * break {@link isSafari17Based} and {@link isSafariSvcApi}, which compare it.
14548
+ */
14549
+ function isIPadOS() {
14550
+ if (!isWeb()) {
14551
+ return false;
14552
+ }
14553
+ const b = getBrowser();
14554
+ return (b === null || b === void 0 ? void 0 : b.name) === 'Safari' && (b === null || b === void 0 ? void 0 : b.os) === 'macOS' && navigator.maxTouchPoints > 1;
14555
+ }
14556
+ /**
14557
+ * iPhone, iPad and iOS-hosted browsers — everything running the Apple camera capture pipeline,
14558
+ * including iPads that present themselves as a Mac.
14559
+ */
14560
+ function isAppleMobile() {
14561
+ var _a;
14562
+ return ((_a = getBrowser()) === null || _a === void 0 ? void 0 : _a.os) === 'iOS' || isIPadOS();
14563
+ }
14387
14564
  function isSafari17Based() {
14388
14565
  const b = getBrowser();
14389
14566
  return (b === null || b === void 0 ? void 0 : b.name) === 'Safari' && b.version.startsWith('17.') || (b === null || b === void 0 ? void 0 : b.os) === 'iOS' && !!(b === null || b === void 0 ? void 0 : b.osVersion) && compareVersions(b.osVersion, '17') >= 0;
@@ -14567,6 +14744,15 @@ function createDummyVideoStreamTrack() {
14567
14744
  return dummyTrack;
14568
14745
  }
14569
14746
  let emptyAudioStreamTrack;
14747
+ let emptyAudioStreamContext;
14748
+ /** tracks handed out by `getEmptyAudioStreamTrack` that haven't been released yet */
14749
+ const outstandingEmptyAudioStreamTracks = new Set();
14750
+ /**
14751
+ * Returns a silent audio track, cloned off a shared `AudioContext` created on first use. Hand the
14752
+ * clone back to `releaseEmptyAudioStreamTrack` rather than stopping it, so the context can close.
14753
+ * A clone that is only stopped stays counted as outstanding, which holds the shared context open
14754
+ * for the lifetime of the page.
14755
+ */
14570
14756
  function getEmptyAudioStreamTrack() {
14571
14757
  if (!emptyAudioStreamTrack) {
14572
14758
  // implementation adapted from https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
@@ -14582,11 +14768,38 @@ function getEmptyAudioStreamTrack() {
14582
14768
  var _dst$stream$getAudioT2 = _slicedToArray(_dst$stream$getAudioT, 1);
14583
14769
  emptyAudioStreamTrack = _dst$stream$getAudioT2[0];
14584
14770
  if (!emptyAudioStreamTrack) {
14771
+ // don't hold on to a context we can't get a track out of
14772
+ ctx.close().catch(() => {});
14585
14773
  throw Error('Could not get empty media stream audio track');
14586
14774
  }
14587
14775
  emptyAudioStreamTrack.enabled = false;
14776
+ emptyAudioStreamContext = ctx;
14588
14777
  }
14589
- return emptyAudioStreamTrack.clone();
14778
+ const track = emptyAudioStreamTrack.clone();
14779
+ outstandingEmptyAudioStreamTracks.add(track);
14780
+ return track;
14781
+ }
14782
+ /**
14783
+ * Stops a track obtained from `getEmptyAudioStreamTrack` and closes the shared `AudioContext`
14784
+ * backing it once every track it handed out has been released. Does nothing for a track this
14785
+ * module never handed out, or one that has been released already.
14786
+ */
14787
+ function releaseEmptyAudioStreamTrack(track) {
14788
+ return __awaiter(this, void 0, void 0, function* () {
14789
+ if (!outstandingEmptyAudioStreamTracks.delete(track)) {
14790
+ // the track wasn't obtained from `getEmptyAudioStreamTrack`, or has been released before
14791
+ return;
14792
+ }
14793
+ track.stop();
14794
+ if (outstandingEmptyAudioStreamTracks.size > 0) {
14795
+ return;
14796
+ }
14797
+ const context = emptyAudioStreamContext;
14798
+ emptyAudioStreamTrack === null || emptyAudioStreamTrack === void 0 ? void 0 : emptyAudioStreamTrack.stop();
14799
+ emptyAudioStreamTrack = undefined;
14800
+ emptyAudioStreamContext = undefined;
14801
+ yield context === null || context === void 0 ? void 0 : context.close();
14802
+ });
14590
14803
  }
14591
14804
  /** An object that represents a serialized version of a `new Promise((resolve, reject) => {})`
14592
14805
  * constructor. Wait for a promise resolution with `await future.promise` and explicitly resolve or
@@ -14650,11 +14863,19 @@ function createAudioAnalyser(track, options) {
14650
14863
  const volume = Math.sqrt(sum / dataArray.length);
14651
14864
  return volume;
14652
14865
  };
14866
+ let isCleanedUp = false;
14653
14867
  const cleanup = () => __awaiter(this, void 0, void 0, function* () {
14654
- yield audioContext.close();
14868
+ if (isCleanedUp) {
14869
+ // closing an already closed context would reject, make repeated cleanups a no-op instead
14870
+ return;
14871
+ }
14872
+ isCleanedUp = true;
14873
+ mediaStreamSource.disconnect();
14874
+ analyser.disconnect();
14655
14875
  if (opts.cloneTrack) {
14656
14876
  streamTrack.stop();
14657
14877
  }
14878
+ yield audioContext.close();
14658
14879
  });
14659
14880
  return {
14660
14881
  calculateVolume,
@@ -14689,6 +14910,31 @@ function unwrapConstraint(constraint) {
14689
14910
  }
14690
14911
  throw Error('could not unwrap constraint');
14691
14912
  }
14913
+ /**
14914
+ * The getUserMedia rejections that no softer constraint can recover from, because the user or the
14915
+ * document refused capture outright. Retrying one of these would put a second prompt in front of
14916
+ * someone who already said no. The last two names are legacy spellings still reported by older
14917
+ * engines. Matched by `name` rather than with `instanceof`, because these are not reliably distinct
14918
+ * constructors across environments. happy-dom, which the tests run under, aliases them to `Event`.
14919
+ */
14920
+ function isPermissionDeniedError(e) {
14921
+ return typeof e === 'object' && e !== null && 'name' in e && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'PermissionDeniedError' || e.name === 'PermissionDismissedError');
14922
+ }
14923
+ const deviceAcquisitionFailures = new WeakSet();
14924
+ /** Records that `e` is the rejection getUserMedia itself produced. */
14925
+ function markDeviceAcquisitionFailure(e) {
14926
+ if (typeof e === 'object' && e !== null) {
14927
+ deviceAcquisitionFailures.add(e);
14928
+ }
14929
+ }
14930
+ /**
14931
+ * Whether a track restart failed at getUserMedia rather than in the work that adopts the new track.
14932
+ * Recorded on the side rather than by wrapping, because callers of `restartTrack` match on the
14933
+ * rejection's own name.
14934
+ */
14935
+ function isDeviceAcquisitionFailure(e) {
14936
+ return typeof e === 'object' && e !== null && deviceAcquisitionFailures.has(e);
14937
+ }
14692
14938
  function toWebsocketUrl(url) {
14693
14939
  if (url.startsWith('http')) {
14694
14940
  return url.replace(/^(http)/, 'ws');
@@ -14875,6 +15121,35 @@ function extractMaxAgeFromRequestHeaders(headers) {
14875
15121
  function isCompressionStreamSupported() {
14876
15122
  return typeof CompressionStream !== 'undefined';
14877
15123
  }
15124
+ let deflateRawCompressionSupported = null;
15125
+ /**
15126
+ * Whether this runtime can compress AND decompress the `deflate-raw` format, which is a stricter
15127
+ * requirement than {@link isCompressionStreamSupported}: Chromium 80-102 included
15128
+ * `CompressionStream` supporting only `gzip` / `deflate`, and `deflate-raw` was added in Chromium
15129
+ * 103 (Firefox 113, Safari 16.4).
15130
+ */
15131
+ function isDeflateRawCompressionSupported() {
15132
+ if (typeof deflateRawCompressionSupported !== 'boolean') {
15133
+ deflateRawCompressionSupported = probeDeflateRawCompressionSupport();
15134
+ }
15135
+ return deflateRawCompressionSupported;
15136
+ }
15137
+ function probeDeflateRawCompressionSupport() {
15138
+ if (typeof CompressionStream === 'undefined' || typeof DecompressionStream === 'undefined') {
15139
+ return false;
15140
+ }
15141
+ try {
15142
+ // There is no api to query format support, and constructing with an unsupported format throws
15143
+ // synchronously, so constructing one of each is the probe. Both are checked because the
15144
+ // deflate-raw capability advertised to peers is as much about decompressing what they send us
15145
+ // as it is about compressing what we send them.
15146
+ new CompressionStream('deflate-raw');
15147
+ new DecompressionStream('deflate-raw');
15148
+ return true;
15149
+ } catch (_a) {
15150
+ return false;
15151
+ }
15152
+ }
14878
15153
  function isPublisherOfferWithJoinSupported() {
14879
15154
  // we have connectivity issue about publisher offer with join on firefox #1919
14880
15155
  return isCompressionStreamSupported() && !isFireFox();
@@ -15877,11 +16152,38 @@ class HTMLElementInfo {
15877
16152
  }
15878
16153
  }
15879
16154
  function isElementInPiP(el) {
15880
- var _a, _b;
16155
+ var _a;
15881
16156
  // Simple video PiP
15882
16157
  if (document.pictureInPictureElement === el) return true;
15883
- // Document PiP
15884
- if ((_a = window.documentPictureInPicture) === null || _a === void 0 ? void 0 : _a.window) return isElementInViewport(el, (_b = window.documentPictureInPicture) === null || _b === void 0 ? void 0 : _b.window);
16158
+ // Document PiP: the element has to actually live in the PiP window, otherwise an
16159
+ // opener-document element whose coordinates happen to fall inside the PiP window's
16160
+ // bounds is reported as being in PiP.
16161
+ const pipWindow = (_a = window.documentPictureInPicture) === null || _a === void 0 ? void 0 : _a.window;
16162
+ if (pipWindow) {
16163
+ return isDocumentInPiPWindow(el.ownerDocument, pipWindow) && isElementInViewport(el, pipWindow);
16164
+ }
16165
+ return false;
16166
+ }
16167
+ // Whether the document is the Document PiP window's own document, or the document of a frame
16168
+ // nested inside it. The spec allows frames in a PiP window, so an element in one is in PiP:
16169
+ // https://wicg.github.io/document-picture-in-picture/#iframes
16170
+ function isDocumentInPiPWindow(doc, pipWindow) {
16171
+ var _a, _b, _c;
16172
+ // A cross-origin frame anywhere between the element and the PiP window hides frameElement
16173
+ // from the walk below, but top stays readable across origins and identifies the top-level
16174
+ // window the element belongs to. For an opener element that is the opener, not the PiP window.
16175
+ if (((_a = doc.defaultView) === null || _a === void 0 ? void 0 : _a.top) === pipWindow) return true;
16176
+ let current = doc;
16177
+ while (current) {
16178
+ if (current === pipWindow.document) return true;
16179
+ try {
16180
+ // Reading frameElement across a cross-origin boundary throws, which means we cannot
16181
+ // establish the ancestry and must not claim the element is in PiP.
16182
+ current = (_c = (_b = current.defaultView) === null || _b === void 0 ? void 0 : _b.frameElement) === null || _c === void 0 ? void 0 : _c.ownerDocument;
16183
+ } catch (_d) {
16184
+ return false;
16185
+ }
16186
+ }
15885
16187
  return false;
15886
16188
  }
15887
16189
  // does not account for occlusion by other elements or opacity property
@@ -16346,12 +16648,16 @@ class E2EEManager extends eventsExports.EventEmitter {
16346
16648
  this.handleReceiver(track.receiver, track.mediaStreamID, remoteId, track.kind === 'video' ? mimeTypeToVideoCodecString(trackInfo.mimeType) : undefined, hasPacketTrailer);
16347
16649
  }
16348
16650
  setupE2EESender(track, sender) {
16349
- var _a, _b, _c;
16651
+ var _a, _b, _c, _d;
16350
16652
  if (!isLocalTrack(track) || !sender) {
16351
16653
  if (!sender) this.log.warn('early return because sender is not ready');
16352
16654
  return;
16353
16655
  }
16354
- this.handleSender(sender, track.mediaStreamID, undefined, isVideoTrack(track) ? (_b = (_a = track.publishOptions) === null || _a === void 0 ? void 0 : _a.frameMetadata) !== null && _b !== void 0 ? _b : (_c = track.publishOptions) === null || _c === void 0 ? void 0 : _c.packetTrailer : undefined);
16656
+ const isVideo = isVideoTrack(track);
16657
+ // Safari exposes no per-frame codec info until trackInfo arrives. Seed only h264/h265:
16658
+ // a wrong vp8 seed would skip the NALU path, a wrong NALU seed only costs a logged fallback.
16659
+ const publishedCodec = isVideo ? (_a = track.publishOptions) === null || _a === void 0 ? void 0 : _a.videoCodec : undefined;
16660
+ this.handleSender(sender, track.mediaStreamID, publishedCodec === 'h264' || publishedCodec === 'h265' ? publishedCodec : undefined, isVideo ? (_c = (_b = track.publishOptions) === null || _b === void 0 ? void 0 : _b.frameMetadata) !== null && _c !== void 0 ? _c : (_d = track.publishOptions) === null || _d === void 0 ? void 0 : _d.packetTrailer : undefined);
16355
16661
  }
16356
16662
  /**
16357
16663
  * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
@@ -17121,6 +17427,58 @@ class AsyncQueue {
17121
17427
  snapshot() {
17122
17428
  return Array.from(this.pendingTasks.values());
17123
17429
  }
17430
+ }/**
17431
+ * Implementation of AbortSignal.any
17432
+ * Creates a signal that will be aborted when any of the given signals is aborted.
17433
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any
17434
+ */
17435
+ function abortSignalAny(signals) {
17436
+ // Handle empty signals array
17437
+ if (signals.length === 0) {
17438
+ const controller = new AbortController();
17439
+ return controller.signal;
17440
+ }
17441
+ // Fast path for single signal
17442
+ if (signals.length === 1) {
17443
+ return signals[0];
17444
+ }
17445
+ // Check if any signal is already aborted
17446
+ for (const signal of signals) {
17447
+ if (signal.aborted) {
17448
+ return signal;
17449
+ }
17450
+ }
17451
+ // Create a new controller for the combined signal
17452
+ const controller = new AbortController();
17453
+ const unlisteners = Array(signals.length);
17454
+ // Function to clean up all event listeners
17455
+ const cleanup = () => {
17456
+ for (const unsubscribe of unlisteners) {
17457
+ unsubscribe();
17458
+ }
17459
+ };
17460
+ // Add event listeners to each signal
17461
+ signals.forEach((signal, index) => {
17462
+ const handler = () => {
17463
+ controller.abort(signal.reason);
17464
+ cleanup();
17465
+ };
17466
+ signal.addEventListener('abort', handler);
17467
+ unlisteners[index] = () => signal.removeEventListener('abort', handler);
17468
+ });
17469
+ return controller.signal;
17470
+ }
17471
+ /**
17472
+ * Implementation of AbortSignal.timeout
17473
+ * Creates a signal that will be aborted after the specified timeout.
17474
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
17475
+ */
17476
+ function abortSignalTimeout(ms) {
17477
+ const controller = new AbortController();
17478
+ setTimeout(() => {
17479
+ controller.abort(new DOMException("signal timed out after ".concat(ms, " ms"), 'TimeoutError'));
17480
+ }, ms);
17481
+ return controller.signal;
17124
17482
  }/**
17125
17483
  * Opt-in registry that lets a development tool observe the state machines driving the connection
17126
17484
  * layer.
@@ -18278,7 +18636,7 @@ class WebSocketStream {
18278
18636
  let _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
18279
18637
  code = _ref.closeCode,
18280
18638
  reason = _ref.reason;
18281
- return ws.close(code, reason);
18639
+ return ws.close(code, reason === undefined ? undefined : truncateCloseReason(reason));
18282
18640
  };
18283
18641
  this.opened = new TypedPromise((resolve, reject) => {
18284
18642
  const rejectHandler = () => {
@@ -18354,6 +18712,25 @@ class WebSocketStream {
18354
18712
  }
18355
18713
  this.close = closeWithInfo;
18356
18714
  }
18715
+ }
18716
+ /** `WebSocket.close` throws a `SyntaxError` for a reason longer than this many UTF-8 bytes. */
18717
+ const MAX_CLOSE_REASON_BYTES = 123;
18718
+ function truncateCloseReason(reason) {
18719
+ const encoder = new TextEncoder();
18720
+ if (encoder.encode(reason).length <= MAX_CLOSE_REASON_BYTES) {
18721
+ return reason;
18722
+ }
18723
+ // cut on code point boundaries, so a multi-byte character is never split
18724
+ let truncated = '';
18725
+ let bytes = 0;
18726
+ for (const char of reason) {
18727
+ bytes += encoder.encode(char).length;
18728
+ if (bytes > MAX_CLOSE_REASON_BYTES) {
18729
+ break;
18730
+ }
18731
+ truncated += char;
18732
+ }
18733
+ return truncated;
18357
18734
  }const passThroughQueueSignals = ['syncState', 'trickle', 'offer', 'answer', 'simulate', 'leave'];
18358
18735
  /**
18359
18736
  * Whether a failed resume may be followed by another one. Mirrors how `RTCEngine` classifies these
@@ -18569,14 +18946,27 @@ class SignalClient {
18569
18946
  let publisherOffer = arguments.length > 5 ? arguments[5] : undefined;
18570
18947
  return function* () {
18571
18948
  const unlock = yield _this2.connectionLock.lock();
18572
- _this2.connectOptions = opts;
18573
- _this2.useV0SignalPath = useV0Path;
18574
- const clientInfo = getClientInfo(opts.clientInfoCapabilities);
18575
- const params = useV0Path ? createConnectionParams(token, clientInfo, opts) : yield createJoinRequestConnectionParams(token, clientInfo, opts, publisherOffer);
18576
- const rtcUrl = createRtcUrl(url, params, useV0Path).toString();
18577
- const validateUrl = createValidateUrl(rtcUrl).toString();
18949
+ let rtcUrl;
18950
+ let validateUrl;
18951
+ try {
18952
+ _this2.connectOptions = opts;
18953
+ _this2.useV0SignalPath = useV0Path;
18954
+ const clientInfo = getClientInfo(opts.clientInfoCapabilities);
18955
+ const params = useV0Path ? createConnectionParams(token, clientInfo, opts) : yield createJoinRequestConnectionParams(token, clientInfo, opts, publisherOffer);
18956
+ rtcUrl = createRtcUrl(url, params, useV0Path).toString();
18957
+ validateUrl = createValidateUrl(rtcUrl).toString();
18958
+ } catch (e) {
18959
+ // the executor below owns the lock from here on
18960
+ unlock();
18961
+ throw e;
18962
+ }
18963
+ if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
18964
+ unlock();
18965
+ _this2.log.debug('signal connect abandoned before opening a transport');
18966
+ throw ConnectionError.cancelled('Connection aborted');
18967
+ }
18578
18968
  return new Promise((resolve, reject) => __awaiter(_this2, void 0, void 0, function* () {
18579
- var _a, _b;
18969
+ var _a, _b, _c, _d, _e;
18580
18970
  try {
18581
18971
  let alreadyAborted = false;
18582
18972
  const abortHandler = eventOrError => __awaiter(this, void 0, void 0, function* () {
@@ -18588,12 +18978,12 @@ class SignalClient {
18588
18978
  const reason = getAbortReasonAsString(target, 'Abort handler called');
18589
18979
  // send leave if we have an active stream writer (connection is open)
18590
18980
  if (this.streamWriter && !this.isDisconnected) {
18591
- this.sendLeave().then(() => this.close(reason)).catch(e => {
18981
+ this.sendLeave().then(() => this.close(true, reason)).catch(e => {
18592
18982
  this.log.error(e);
18593
- this.close();
18983
+ this.close(true, reason);
18594
18984
  });
18595
18985
  } else {
18596
- this.close();
18986
+ this.close(true, reason);
18597
18987
  }
18598
18988
  cleanupAbortHandlers();
18599
18989
  // The caller may already have classified this failure: the connect timeout hands us a
@@ -18619,6 +19009,10 @@ class SignalClient {
18619
19009
  yield this.teardownTransport('replaced by a new connection attempt');
18620
19010
  this.log.debug("closed previous ws connection in ".concat(performance.now() - startClose, "ms"));
18621
19011
  }
19012
+ if (alreadyAborted) {
19013
+ this.log.debug('signal connect abandoned after tearing down the previous transport');
19014
+ return;
19015
+ }
18622
19016
  // the transport created below belongs to this attempt; events arriving from it after a
18623
19017
  // newer attempt has started are dropped by the machine
18624
19018
  const attemptId = this.attemptId;
@@ -18663,6 +19057,12 @@ class SignalClient {
18663
19057
  }
18664
19058
  });
18665
19059
  const connection = yield this.ws.opened.catch(reason => __awaiter(this, void 0, void 0, function* () {
19060
+ if (alreadyAborted) {
19061
+ // the abort handler has already rejected this attempt; don't probe the validate
19062
+ // endpoint for a failure nobody is waiting on
19063
+ this.log.debug('signal connect abandoned while the websocket was opening');
19064
+ return;
19065
+ }
18666
19066
  if (this.lifecycleState !== 'connected') {
18667
19067
  // claimed synchronously, before the await below: the socket's close event is already
18668
19068
  // on its way and would otherwise reject first with a less useful error
@@ -18683,38 +19083,63 @@ class SignalClient {
18683
19083
  const signalReader = connection.readable.getReader();
18684
19084
  this.streamWriter = connection.writable.getWriter();
18685
19085
  // wsTimeout only guarded the upgrade; guard the first-message read with
18686
- // its own timeout so a silent server can't hang join() forever.
18687
- let firstMessage;
18688
- let firstMessageTimeout;
19086
+ // its own deadline so a silent server can't hang join() forever.
19087
+ // During reconnect, drop any stray messages that arrive before the
19088
+ // ReconnectResponse so we do not declare the channel reconnected on the
19089
+ // wrong signal.
19090
+ let firstSignalResponse;
18689
19091
  try {
18690
- firstMessage = yield Promise.race([signalReader.read(), new Promise((_, rejectRead) => {
18691
- firstMessageTimeout = setTimeout(() => {
18692
- rejectRead(ConnectionError.timeout('signal connection timed out while waiting for the first message'));
18693
- }, JOIN_RESPONSE_TIMEOUT);
18694
- })]);
19092
+ const deadline = Date.now() + JOIN_RESPONSE_TIMEOUT;
19093
+ // eslint-disable-next-line no-constant-condition
19094
+ while (true) {
19095
+ const remaining = deadline - Date.now();
19096
+ if (remaining <= 0) {
19097
+ throw ConnectionError.timeout('signal connection timed out while waiting for the first message');
19098
+ }
19099
+ let readTimeout;
19100
+ const readResult = yield Promise.race([signalReader.read(), new Promise((_, rejectRead) => {
19101
+ readTimeout = setTimeout(() => {
19102
+ rejectRead(ConnectionError.timeout('signal connection timed out while waiting for the first message'));
19103
+ }, remaining);
19104
+ })]);
19105
+ clearTimeout(readTimeout);
19106
+ if (!readResult.value) {
19107
+ throw ConnectionError.internal('no message received as first message');
19108
+ }
19109
+ const parsed = parseSignalResponse(readResult.value);
19110
+ if (this.lifecycleState === 'reconnecting' && ((_a = parsed.message) === null || _a === void 0 ? void 0 : _a.case) !== 'reconnect' && ((_b = parsed.message) === null || _b === void 0 ? void 0 : _b.case) !== 'leave') {
19111
+ this.log.warn('dropping signal message while awaiting reconnect response', {
19112
+ messageCase: (_c = parsed.message) === null || _c === void 0 ? void 0 : _c.case
19113
+ });
19114
+ continue;
19115
+ }
19116
+ firstSignalResponse = parsed;
19117
+ break;
19118
+ }
18695
19119
  } catch (e) {
18696
- // No first message in time: release the reader and tear down the ws
19120
+ // No usable first message in time: release the reader and tear down the ws
18697
19121
  // so we surface the timeout instead of leaking an open connection.
18698
19122
  signalReader.releaseLock();
18699
19123
  reject(e);
18700
19124
  this.close();
18701
19125
  return;
18702
- } finally {
18703
- clearTimeout(firstMessageTimeout);
18704
19126
  }
18705
19127
  signalReader.releaseLock();
18706
- if (!firstMessage.value) {
18707
- throw ConnectionError.internal('no message received as first message');
19128
+ if (alreadyAborted) {
19129
+ // The abort handler has already rejected this attempt and is closing the transport.
19130
+ // Don't act on a join that raced it: `onJoined` would publish an abandoned session's
19131
+ // state to the room.
19132
+ this.log.debug('signal connect abandoned while waiting for the first message');
19133
+ return;
18708
19134
  }
18709
- const firstSignalResponse = parseSignalResponse(firstMessage.value);
18710
19135
  // Validate the first message
18711
- const validation = this.validateFirstMessage(firstSignalResponse, (_a = opts.reconnect) !== null && _a !== void 0 ? _a : false);
19136
+ const validation = this.validateFirstMessage(firstSignalResponse, (_d = opts.reconnect) !== null && _d !== void 0 ? _d : false);
18712
19137
  if (!validation.isValid) {
18713
19138
  reject(validation.error);
18714
19139
  return;
18715
19140
  }
18716
19141
  // Handle join response
18717
- if (((_b = firstSignalResponse.message) === null || _b === void 0 ? void 0 : _b.case) === 'join') {
19142
+ if (((_e = firstSignalResponse.message) === null || _e === void 0 ? void 0 : _e.case) === 'join') {
18718
19143
  // Set up ping configuration
18719
19144
  this.pingTimeoutDuration = firstSignalResponse.message.value.pingTimeout;
18720
19145
  this.pingIntervalDuration = firstSignalResponse.message.value.pingInterval;
@@ -18728,9 +19153,7 @@ class SignalClient {
18728
19153
  this.onJoined(firstSignalResponse.message.value);
18729
19154
  }
18730
19155
  }
18731
- // Handle successful connection
18732
- const firstMessageToProcess = validation.shouldProcessFirstMessage ? firstSignalResponse : undefined;
18733
- this.handleSignalConnected(connection, wsTimeout, attemptId, firstMessageToProcess);
19156
+ this.handleSignalConnected(connection, wsTimeout, attemptId);
18734
19157
  resolve(validation.response);
18735
19158
  } catch (e) {
18736
19159
  reject(e);
@@ -18744,11 +19167,8 @@ class SignalClient {
18744
19167
  }();
18745
19168
  });
18746
19169
  }
18747
- startReadingLoop(signalReader, firstMessage) {
19170
+ startReadingLoop(signalReader) {
18748
19171
  return __awaiter(this, void 0, void 0, function* () {
18749
- if (firstMessage) {
18750
- this.handleSignalResponse(firstMessage);
18751
- }
18752
19172
  const attemptId = this.attemptId;
18753
19173
  while (true) {
18754
19174
  if (this.signalLatency) {
@@ -18808,16 +19228,17 @@ class SignalClient {
18808
19228
  return __awaiter(this, void 0, void 0, function* () {
18809
19229
  try {
18810
19230
  this.clearPingInterval();
18811
- if (this.ws) {
18812
- this.ws.close({
19231
+ const ws = this.ws;
19232
+ if (ws) {
19233
+ // released before closing, so a close that throws cannot leave the transport attached
19234
+ this.ws = undefined;
19235
+ this.streamWriter = undefined;
19236
+ ws.close({
18813
19237
  closeCode: 1000,
18814
19238
  reason
18815
19239
  });
18816
19240
  // calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
18817
- const closePromise = this.ws.closed;
18818
- this.ws = undefined;
18819
- this.streamWriter = undefined;
18820
- yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
19241
+ yield Promise.race([ws.closed, sleep(MAX_WS_CLOSE_TIME)]);
18821
19242
  }
18822
19243
  } catch (e) {
18823
19244
  this.log.debug('websocket error while closing', {
@@ -19184,6 +19605,7 @@ class SignalClient {
19184
19605
  return __awaiter(this, arguments, void 0, function (reason) {
19185
19606
  var _this6 = this;
19186
19607
  let attemptId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.attemptId;
19608
+ let simulateReconnectDelay = arguments.length > 2 ? arguments[2] : undefined;
19187
19609
  return function* () {
19188
19610
  const onCloseCallback = _this6.onClose;
19189
19611
  if (!_this6.sendLifecycleInput({
@@ -19205,6 +19627,10 @@ class SignalClient {
19205
19627
  _this6.log.info("websocket connection closed: ".concat(reason), {
19206
19628
  reason
19207
19629
  });
19630
+ if (simulateReconnectDelay) {
19631
+ _this6.log.info("waiting for ".concat(simulateReconnectDelay, "ms until reconnecting signal"));
19632
+ yield sleep(simulateReconnectDelay);
19633
+ }
19208
19634
  if (onCloseCallback) {
19209
19635
  onCloseCallback(reason);
19210
19636
  }
@@ -19265,7 +19691,7 @@ class SignalClient {
19265
19691
  * @param firstMessage Optional first message to process
19266
19692
  * @internal
19267
19693
  */
19268
- handleSignalConnected(connection, timeoutHandle, attemptId, firstMessage) {
19694
+ handleSignalConnected(connection, timeoutHandle, attemptId) {
19269
19695
  clearTimeout(timeoutHandle);
19270
19696
  const established = this.sendLifecycleInput(this.lifecycleState === 'reconnecting' ? {
19271
19697
  type: 'reconnectComplete',
@@ -19287,7 +19713,7 @@ class SignalClient {
19287
19713
  }
19288
19714
  this.log.info('signal connected');
19289
19715
  this.startPingInterval();
19290
- this.startReadingLoop(connection.readable.getReader(), firstMessage);
19716
+ this.startReadingLoop(connection.readable.getReader());
19291
19717
  }
19292
19718
  /**
19293
19719
  * Validates the first message received from the signal server
@@ -19297,28 +19723,18 @@ class SignalClient {
19297
19723
  * @internal
19298
19724
  */
19299
19725
  validateFirstMessage(firstSignalResponse, isReconnect) {
19300
- var _a, _b, _c, _d, _e;
19726
+ var _a, _b, _c, _d;
19301
19727
  if (((_a = firstSignalResponse.message) === null || _a === void 0 ? void 0 : _a.case) === 'join') {
19302
19728
  return {
19303
19729
  isValid: true,
19304
19730
  response: firstSignalResponse.message.value
19305
19731
  };
19306
- } else if (this.lifecycleState === 'reconnecting' && ((_b = firstSignalResponse.message) === null || _b === void 0 ? void 0 : _b.case) !== 'leave') {
19307
- if (((_c = firstSignalResponse.message) === null || _c === void 0 ? void 0 : _c.case) === 'reconnect') {
19308
- return {
19309
- isValid: true,
19310
- response: firstSignalResponse.message.value
19311
- };
19312
- } else {
19313
- // in reconnecting, any message received means signal reconnected and we still need to process it
19314
- this.log.debug('declaring signal reconnected without reconnect response received');
19315
- return {
19316
- isValid: true,
19317
- response: undefined,
19318
- shouldProcessFirstMessage: true
19319
- };
19320
- }
19321
- } else if (this.isEstablishingConnection && ((_d = firstSignalResponse.message) === null || _d === void 0 ? void 0 : _d.case) === 'leave') {
19732
+ } else if (this.lifecycleState === 'reconnecting' && ((_b = firstSignalResponse.message) === null || _b === void 0 ? void 0 : _b.case) === 'reconnect') {
19733
+ return {
19734
+ isValid: true,
19735
+ response: firstSignalResponse.message.value
19736
+ };
19737
+ } else if (this.isEstablishingConnection && ((_c = firstSignalResponse.message) === null || _c === void 0 ? void 0 : _c.case) === 'leave') {
19322
19738
  return {
19323
19739
  isValid: false,
19324
19740
  error: ConnectionError.leaveRequest('Received leave request while trying to (re)connect', firstSignalResponse.message.value.reason)
@@ -19327,7 +19743,7 @@ class SignalClient {
19327
19743
  // non-reconnect case, should receive join response first
19328
19744
  return {
19329
19745
  isValid: false,
19330
- error: ConnectionError.internal("did not receive join response, got ".concat((_e = firstSignalResponse.message) === null || _e === void 0 ? void 0 : _e.case, " instead"))
19746
+ error: ConnectionError.internal("did not receive join response, got ".concat((_d = firstSignalResponse.message) === null || _d === void 0 ? void 0 : _d.case, " instead"))
19331
19747
  };
19332
19748
  }
19333
19749
  return {
@@ -19345,7 +19761,9 @@ class SignalClient {
19345
19761
  handleConnectionError(reason, validateUrl) {
19346
19762
  return __awaiter(this, void 0, void 0, function* () {
19347
19763
  try {
19348
- const resp = yield fetch(validateUrl);
19764
+ const resp = yield fetch(validateUrl, {
19765
+ signal: abortSignalTimeout(3000)
19766
+ });
19349
19767
  switch (resp.status) {
19350
19768
  case 404:
19351
19769
  const errorMsg = yield resp.text();
@@ -19369,7 +19787,11 @@ class SignalClient {
19369
19787
  });
19370
19788
  }
19371
19789
  } catch (e) {
19372
- return e instanceof ConnectionError ? e : ConnectionError.serverUnreachable(e instanceof Error ? e.message : 'server was not reachable');
19790
+ if (e instanceof ConnectionError) {
19791
+ return e;
19792
+ } else {
19793
+ return ConnectionError.serverUnreachable(e instanceof Error ? e.message : 'server was not reachable');
19794
+ }
19373
19795
  }
19374
19796
  });
19375
19797
  }
@@ -19432,6 +19854,12 @@ function createConnectionParams(token, info, opts) {
19432
19854
  if (opts.adaptiveStream) {
19433
19855
  params.set('adaptive_stream', '1');
19434
19856
  }
19857
+ if (opts.disableIceLite) {
19858
+ params.set('disable_ice_lite', '1');
19859
+ }
19860
+ if (typeof opts.subscriberAllowPause !== 'undefined') {
19861
+ params.set('subscriber_allow_pause', opts.subscriberAllowPause ? '1' : '0');
19862
+ }
19435
19863
  if (opts.reconnectReason) {
19436
19864
  params.set('reconnect_reason', opts.reconnectReason.toString());
19437
19865
  }
@@ -19450,7 +19878,9 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
19450
19878
  clientInfo: info,
19451
19879
  connectionSettings: new ConnectionSettings({
19452
19880
  autoSubscribe: !!opts.autoSubscribe,
19453
- adaptiveStream: !!opts.adaptiveStream
19881
+ adaptiveStream: !!opts.adaptiveStream,
19882
+ disableIceLite: !!opts.disableIceLite,
19883
+ subscriberAllowPause: opts.subscriberAllowPause
19454
19884
  }),
19455
19885
  reconnect: !!opts.reconnect,
19456
19886
  participantSid: opts.sid ? opts.sid : undefined,
@@ -20287,68 +20717,206 @@ function requireLib() {
20287
20717
  * Why same for all codecs: Target bitrate already accounts for codec efficiency
20288
20718
  * (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
20289
20719
  * Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
20720
+ * The cap is lowered further for connections that were slow to set up, see
20721
+ * `computeStartBitrateCap`, or that the browser reports as slow, see
20722
+ * `connectionEffectiveTypeStartBitrateCap`.
20290
20723
  */
20291
20724
  const startBitrateMultiplier = 0.9;
20292
20725
  /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
20293
20726
  const maxStartBitrateKbps = 1000;
20727
+ /**
20728
+ * Minimum x-google-start-bitrate in kbps: libwebrtc's own default starting estimate. A target
20729
+ * below this gets no hint, since seeding above the real capacity costs more than the ramp it
20730
+ * saves, and a slow connection is seeded no higher than this.
20731
+ */
20732
+ const minTargetBitrateKbps = 300;
20733
+ /**
20734
+ * Connection setup times (ms) bounding the ramp in `computeStartBitrateCap`: at or below the
20735
+ * first the hint is capped at `maxStartBitrateKbps`, at or above the second at
20736
+ * `minTargetBitrateKbps`. Measured on shaped links (CLT-3380): healthy links set up in under
20737
+ * 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,
20738
+ * and a 300 kbps link never under 3 s. The first sits above the 1 Mbps link, which a lower seed
20739
+ * only slows down, and the second above the 500 kbps link's median, so only links that cannot
20740
+ * carry more land at the floor.
20741
+ */
20742
+ const setupTimeForMaxStartBitrateMs = 1500;
20743
+ const setupTimeForMinStartBitrateMs = 3500;
20744
+ /** Cap on the start bitrate hint, in kbps, when the browser reports a `3g` connection. */
20745
+ const effectiveType3gStartBitrateKbps = 500;
20294
20746
  const debounceInterval = 20;
20295
20747
  /**
20296
- * Applies the configured start bitrate when this media section belongs to `cid`.
20297
- * This SDP munging is used for a bitrate setting that cannot be applied through
20298
- * `RTCRtpEncodingParameters`.
20748
+ * Codec payload for `codec` in this media section, when the section carries `cid`.
20299
20749
  *
20300
- * Returns `undefined` when the section does not belong to the track, `0` when
20301
- * it does but does not offer the requested codec, and the codec payload when the
20302
- * requested codec is present (whether the bitrate was added or already set).
20750
+ * Returns `undefined` when the section does not belong to the track, `0` when it does but
20751
+ * does not offer the requested codec, and the codec payload otherwise.
20303
20752
  *
20304
20753
  * @internal
20305
20754
  */
20306
- function applyVideoStartBitrate(media, cid, codec, maxbr) {
20307
- let isScreenShare = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
20755
+ function findTrackCodecPayload(media, cid, codec) {
20308
20756
  var _a, _b, _c;
20309
20757
  if (!((_a = media.msid) === null || _a === void 0 ? void 0 : _a.includes(cid))) {
20310
20758
  return undefined;
20311
20759
  }
20312
- const codecPayload = (_c = (_b = media.rtp.find(rtp => rtp.codec.toUpperCase() === codec.toUpperCase())) === null || _b === void 0 ? void 0 : _b.payload) !== null && _c !== void 0 ? _c : 0;
20313
- if (codecPayload === 0) {
20314
- return 0;
20760
+ return (_c = (_b = media.rtp.find(rtp => rtp.codec.toUpperCase() === codec.toUpperCase())) === null || _b === void 0 ? void 0 : _b.payload) !== null && _c !== void 0 ? _c : 0;
20761
+ }
20762
+ /**
20763
+ * Start bitrate hinted for a single track, or `undefined` when its target is too low to
20764
+ * be worth seeding.
20765
+ *
20766
+ * 90% of the target leaves ~10% headroom for the estimator to settle. The same multiplier
20767
+ * is used for every codec because the target already reflects the codec's efficiency.
20768
+ * Camera is capped at 1 Mbps so the estimator does not open too aggressively on a
20769
+ * high-bitrate track; screen share is exempt, because its content needs the bitrate
20770
+ * immediately to stay legible.
20771
+ *
20772
+ * The cap is lowered for a connection that was slow to set up (`computeStartBitrateCap`), and
20773
+ * for one the browser reports as slow (`connectionEffectiveTypeStartBitrateCap`), whichever is
20774
+ * lower. Screen share is exempt from both, however slow the connection.
20775
+ *
20776
+ * @internal
20777
+ */
20778
+ function computeTrackStartBitrate(trackbr, connectionSetupTimeMs) {
20779
+ if (trackbr.maxbr < minTargetBitrateKbps) {
20780
+ return undefined;
20315
20781
  }
20316
- // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
20317
- // from starting too aggressively. Screen share is not capped since text/UI
20318
- // clarity requires high bitrate from the start.
20319
- // TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
20320
- const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
20321
- const startBitrate = isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
20322
- const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
20323
- if (fmtp) {
20324
- // If another track's fmtp already has a start bitrate, it cannot be
20325
- // overridden here because the payload type is shared across the bundle.
20326
- // This forces every track sharing that payload to use the initial track's
20327
- // start bitrate.
20328
- if (!fmtp.config.includes('x-google-start-bitrate')) {
20329
- fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
20330
- }
20331
- } else {
20332
- // VP8 and some codecs may not have an existing fmtp line.
20333
- media.fmtp.push({
20334
- payload: codecPayload,
20335
- config: "x-google-start-bitrate=".concat(startBitrate)
20336
- });
20782
+ const calculated = Math.min(Math.round(trackbr.maxbr * startBitrateMultiplier), trackbr.maxbr);
20783
+ if (trackbr.isScreenShare) {
20784
+ return calculated;
20337
20785
  }
20338
- return codecPayload;
20786
+ const cap = Math.min(computeStartBitrateCap(connectionSetupTimeMs), connectionEffectiveTypeStartBitrateCap());
20787
+ return Math.min(calculated, cap);
20339
20788
  }
20340
- const PCEvents = {
20341
- NegotiationStarted: 'negotiationStarted',
20342
- NegotiationComplete: 'negotiationComplete',
20343
- // Fired with the offerId for every successful publisher answer application,
20344
- // including answers that immediately recurse into another offer via
20345
- // `renegotiate`. Use this rather than NegotiationComplete to know that a
20346
- // specific offer has been negotiated end-to-end.
20347
- OfferAnswered: 'offerAnswered',
20348
- RTPVideoPayloadTypes: 'rtpVideoPayloadTypes'
20349
- };
20350
- /** @internal */
20351
- class PCTransport extends eventsExports.EventEmitter {
20789
+ /**
20790
+ * Cap on the start bitrate hint, in kbps, from the browser's effective connection type
20791
+ * (`navigator.connection.effectiveType`, Chromium only): `slow-2g` and `2g` cap at
20792
+ * `minTargetBitrateKbps`, `3g` at `effectiveType3gStartBitrateKbps`.
20793
+ *
20794
+ * It catches links that have low bandwidth but low latency, which set up quickly and so slip
20795
+ * past the setup time ramp. `4g`, and browsers without the API, give no guidance and stay at the
20796
+ * `maxStartBitrateKbps` ceiling, as `computeStartBitrateCap` does without a setup time. The
20797
+ * connection RTT the API also reports is not used: it lags behind network changes, and the setup
20798
+ * time already reflects latency.
20799
+ */
20800
+ function connectionEffectiveTypeStartBitrateCap() {
20801
+ // A `typeof` guard, since `globalThis` is missing from some supported browsers (Chrome < 71,
20802
+ // Firefox < 65, Safari < 12.1) and the build does not polyfill it.
20803
+ const connection = typeof navigator !== 'undefined' ? navigator.connection : undefined;
20804
+ switch (connection === null || connection === void 0 ? void 0 : connection.effectiveType) {
20805
+ case 'slow-2g':
20806
+ case '2g':
20807
+ return minTargetBitrateKbps;
20808
+ case '3g':
20809
+ return effectiveType3gStartBitrateKbps;
20810
+ default:
20811
+ return maxStartBitrateKbps;
20812
+ }
20813
+ }
20814
+ /**
20815
+ * Cap on the start bitrate hint, in kbps, from how long the connection took to set up.
20816
+ *
20817
+ * Connection setup time (signaling join plus ICE/DTLS) is the network signal every platform has
20818
+ * before the first video offer, since libwebrtc cannot probe the path until a video sender
20819
+ * exists. It grows with round-trip time and loss, which also mark the links where a 1 Mbps
20820
+ * seed overshoots, so the cap ramps linearly from `maxStartBitrateKbps` at
20821
+ * `setupTimeForMaxStartBitrateMs` down to `minTargetBitrateKbps` at
20822
+ * `setupTimeForMinStartBitrateMs`. A video offer built while the connection is still being set
20823
+ * up (publishing during connect) passes the time elapsed so far, a lower bound on the setup time.
20824
+ * Without any timing the cap stays at the 1 Mbps ceiling.
20825
+ */
20826
+ function computeStartBitrateCap(connectionSetupTimeMs) {
20827
+ if (connectionSetupTimeMs === undefined) {
20828
+ return maxStartBitrateKbps;
20829
+ }
20830
+ const ramp = Math.min(Math.max((connectionSetupTimeMs - setupTimeForMaxStartBitrateMs) / (setupTimeForMinStartBitrateMs - setupTimeForMaxStartBitrateMs), 0), 1);
20831
+ return Math.round(maxStartBitrateKbps - ramp * (maxStartBitrateKbps - minTargetBitrateKbps));
20832
+ }
20833
+ /**
20834
+ * The single start bitrate for this peer connection: the largest hint among the video
20835
+ * m-sections of `media` that map to a published track.
20836
+ *
20837
+ * libwebrtc reads `x-google-start-bitrate` per m-section but applies it to the shared
20838
+ * `Call` (`WebRtcVideoSendChannel::ApplyChangedParams` -> `SetSdpBitrateParameters`), where
20839
+ * `RtpBitrateConfigurator` holds one config for the whole connection. Differing per-section
20840
+ * values are therefore last-writer-wins, decided by m-section order, so every video section
20841
+ * gets the same number instead.
20842
+ *
20843
+ * Only sections that can send local media are considered. `trackBitrates` is append-only and an
20844
+ * unpublished section keeps its `a=msid`, so matching on msid alone would still pair a stale
20845
+ * entry with its old section — letting an uncapped screen-share target seed a connection that
20846
+ * now carries only a camera, or consuming the one-shot hint on a section that sends nothing.
20847
+ *
20848
+ * The direction is the discriminator, as an exclusion rather than a match: `recvonly` and
20849
+ * `inactive` are the only directions that cannot carry local media, and they are exactly the
20850
+ * two a dead section lands on — unpublish sets `inactive` (explicitly in
20851
+ * `LocalParticipant.unpublishTrack`, and via `removeTrack`'s sendonly -> inactive transition for
20852
+ * the simulcast senders), `removeTrack` on a `sendrecv` transceiver leaves `recvonly`, and the
20853
+ * pre-populated placeholders are `recvonly`. Everything else sends: `sendonly` from the
20854
+ * `addTransceiver` path, `sendrecv` from the legacy `addTrack` fallback (which reuses a
20855
+ * transceiver rather than creating a sendonly one), and a section with no direction attribute,
20856
+ * which SDP defaults to `sendrecv`.
20857
+ *
20858
+ * @internal
20859
+ */
20860
+ function computeConnectionStartBitrate(media, trackBitrates, connectionSetupTimeMs) {
20861
+ let connectionStartBitrate;
20862
+ for (const m of media) {
20863
+ if (m.type !== 'video' || m.direction === 'recvonly' || m.direction === 'inactive') {
20864
+ continue;
20865
+ }
20866
+ for (const trackbr of trackBitrates) {
20867
+ if (!trackbr.cid) {
20868
+ continue;
20869
+ }
20870
+ const codecPayload = findTrackCodecPayload(m, trackbr.cid, trackbr.codec);
20871
+ if (codecPayload === undefined) {
20872
+ continue;
20873
+ }
20874
+ const startBitrate = codecPayload > 0 ? computeTrackStartBitrate(trackbr, connectionSetupTimeMs) : undefined;
20875
+ if (startBitrate !== undefined && (connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)) {
20876
+ connectionStartBitrate = startBitrate;
20877
+ }
20878
+ break;
20879
+ }
20880
+ }
20881
+ return connectionStartBitrate;
20882
+ }
20883
+ /**
20884
+ * Declares `x-google-start-bitrate` on `codecPayload`'s fmtp. This SDP munging is used for
20885
+ * a bitrate setting that cannot be applied through `RTCRtpEncodingParameters`.
20886
+ *
20887
+ * Returns whether the section now carries the hint.
20888
+ *
20889
+ * @internal
20890
+ */
20891
+ function applyVideoStartBitrate(media, codecPayload, startBitrate) {
20892
+ const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
20893
+ if (fmtp) {
20894
+ // A payload type is shared across the bundle, so a value written for one section is
20895
+ // already the connection-level one; leave it rather than rewrite it.
20896
+ if (!fmtp.config.includes('x-google-start-bitrate')) {
20897
+ fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
20898
+ }
20899
+ } else {
20900
+ // VP8 and some codecs may not have an existing fmtp line.
20901
+ media.fmtp.push({
20902
+ payload: codecPayload,
20903
+ config: "x-google-start-bitrate=".concat(startBitrate)
20904
+ });
20905
+ }
20906
+ return true;
20907
+ }
20908
+ const PCEvents = {
20909
+ NegotiationStarted: 'negotiationStarted',
20910
+ NegotiationComplete: 'negotiationComplete',
20911
+ // Fired with the offerId for every successful publisher answer application,
20912
+ // including answers that immediately recurse into another offer via
20913
+ // `renegotiate`. Use this rather than NegotiationComplete to know that a
20914
+ // specific offer has been negotiated end-to-end.
20915
+ OfferAnswered: 'offerAnswered',
20916
+ RTPVideoPayloadTypes: 'rtpVideoPayloadTypes'
20917
+ };
20918
+ /** @internal */
20919
+ class PCTransport extends eventsExports.EventEmitter {
20352
20920
  get pc() {
20353
20921
  if (!this._pc) {
20354
20922
  this._pc = this.createPC();
@@ -20363,11 +20931,20 @@ class PCTransport extends eventsExports.EventEmitter {
20363
20931
  this.iceLog = livekitLogger;
20364
20932
  this.ddExtID = 0;
20365
20933
  this.latestOfferId = 0;
20934
+ this.answeredOfferId = 0;
20366
20935
  this.latestAcknowledgedOfferId = 0;
20367
20936
  this.pendingCandidates = [];
20368
20937
  this.restartingIce = false;
20369
20938
  this.renegotiate = false;
20370
20939
  this.trackBitrates = [];
20940
+ /**
20941
+ * Whether an offer carrying the connection-level `x-google-start-bitrate` has been
20942
+ * accepted locally. The hint is written once per peer connection: libwebrtc retains
20943
+ * `start_bitrate_bps` in `RtpBitrateConfigurator` and re-applies it on network route
20944
+ * changes, so a later rewrite is at best a no-op and at worst restarts a converged
20945
+ * bandwidth estimator. A new peer connection (full reconnect) seeds a new estimator.
20946
+ */
20947
+ this.hasAppliedVideoStartBitrate = false;
20371
20948
  this.remoteStereoMids = [];
20372
20949
  this.remoteNackMids = [];
20373
20950
  // debounced negotiate interface
@@ -20476,7 +21053,7 @@ class PCTransport extends eventsExports.EventEmitter {
20476
21053
  }
20477
21054
  setRemoteDescription(sd, offerId) {
20478
21055
  return __awaiter(this, void 0, void 0, function* () {
20479
- var _a, _b;
21056
+ var _a;
20480
21057
  if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
20481
21058
  this.log.warn('ignoring answer for old offer', {
20482
21059
  offerId,
@@ -20484,6 +21061,29 @@ class PCTransport extends eventsExports.EventEmitter {
20484
21061
  });
20485
21062
  return false;
20486
21063
  }
21064
+ if (sd.type === 'answer') {
21065
+ if (isDuplicateAnswer(offerId, this.answerInFlight, this.answeredOfferId, (_a = this._pc) === null || _a === void 0 ? void 0 : _a.signalingState, this.pendingInitialOffer !== undefined)) {
21066
+ // a repeat of an answer already applied or being applied (e.g.
21067
+ // duplicated by a proxy): there is no local offer left to answer
21068
+ this.log.warn('ignoring duplicate answer', {
21069
+ offerId
21070
+ });
21071
+ return false;
21072
+ }
21073
+ // claimed before any await, so a copy arriving mid-apply is caught too;
21074
+ // released however the apply ends, so a genuine retry is still possible
21075
+ this.answerInFlight = offerId;
21076
+ }
21077
+ try {
21078
+ return yield this.applyRemoteDescription(sd, offerId);
21079
+ } finally {
21080
+ if (sd.type === 'answer') this.answerInFlight = undefined;
21081
+ }
21082
+ });
21083
+ }
21084
+ applyRemoteDescription(sd, offerId) {
21085
+ return __awaiter(this, void 0, void 0, function* () {
21086
+ var _a, _b;
20487
21087
  let mungedSDP = undefined;
20488
21088
  if (sd.type === 'offer') {
20489
21089
  let _extractStereoAndNack = extractStereoAndNackAudioFromOffer(sd),
@@ -20557,6 +21157,8 @@ class PCTransport extends eventsExports.EventEmitter {
20557
21157
  mungedSDP = libExports.write(sdpParsed);
20558
21158
  }
20559
21159
  yield this.setMungedSDP(sd, mungedSDP, true);
21160
+ // only a successfully applied answer makes later copies duplicates
21161
+ if (sd.type === 'answer' && offerId > 0) this.answeredOfferId = offerId;
20560
21162
  if (this.pendingCandidates.length > 0) {
20561
21163
  this.iceLog.debug('flushing queued ICE candidates', {
20562
21164
  count: this.pendingCandidates.length
@@ -20664,6 +21266,12 @@ class PCTransport extends eventsExports.EventEmitter {
20664
21266
  sdp: offer.sdp
20665
21267
  });
20666
21268
  const sdpParsed = libExports.parse((_a = offer.sdp) !== null && _a !== void 0 ? _a : '');
21269
+ // One value for every video m-section, written only on the first offer that carries
21270
+ // local video: the hint is connection-level in libwebrtc, so differing per-section
21271
+ // values would be last-writer-wins on m-section order. Offers before any video is
21272
+ // published (data channel or audio only) find no target and leave the latch unset.
21273
+ const connectionStartBitrate = this.hasAppliedVideoStartBitrate ? undefined : computeConnectionStartBitrate(sdpParsed.media, this.trackBitrates, this.connectionSetupTimeEstimateMs());
21274
+ let appliedVideoStartBitrate = false;
20667
21275
  sdpParsed.media.forEach(media => {
20668
21276
  ensureIPAddrMatchVersion(media);
20669
21277
  if (media.type === 'audio') {
@@ -20673,12 +21281,17 @@ class PCTransport extends eventsExports.EventEmitter {
20673
21281
  if (!trackbr.cid) {
20674
21282
  return false;
20675
21283
  }
20676
- const codecPayload = applyVideoStartBitrate(media, trackbr.cid, trackbr.codec, trackbr.maxbr, trackbr.isScreenShare);
21284
+ const codecPayload = findTrackCodecPayload(media, trackbr.cid, trackbr.codec);
20677
21285
  if (codecPayload === undefined) {
20678
21286
  return false;
20679
21287
  }
20680
- if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
20681
- this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
21288
+ if (codecPayload > 0) {
21289
+ if (connectionStartBitrate !== undefined) {
21290
+ appliedVideoStartBitrate = applyVideoStartBitrate(media, codecPayload, connectionStartBitrate) || appliedVideoStartBitrate;
21291
+ }
21292
+ if (isSVCCodec(trackbr.codec) && !isSafari()) {
21293
+ this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
21294
+ }
20682
21295
  }
20683
21296
  return true;
20684
21297
  });
@@ -20700,7 +21313,17 @@ class PCTransport extends eventsExports.EventEmitter {
20700
21313
  });
20701
21314
  return;
20702
21315
  }
20703
- yield this.setMungedSDP(offer, libExports.write(sdpParsed));
21316
+ const mungedSdp = libExports.write(sdpParsed);
21317
+ yield this.setMungedSDP(offer, mungedSdp);
21318
+ // setMungedSDP falls back to the unmunged SDP on rejection. Only consume the
21319
+ // one-shot hint once the SDP carrying it has been accepted locally.
21320
+ if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
21321
+ this.hasAppliedVideoStartBitrate = true;
21322
+ this.log.info('applied x-google-start-bitrate', {
21323
+ startBitrateKbps: connectionStartBitrate,
21324
+ connectionSetupTimeMs: this.connectionSetupTimeEstimateMs()
21325
+ });
21326
+ }
20704
21327
  this.onOffer(offer, this.latestOfferId);
20705
21328
  } finally {
20706
21329
  unlock();
@@ -20751,6 +21374,27 @@ class PCTransport extends eventsExports.EventEmitter {
20751
21374
  setTrackCodecBitrate(info) {
20752
21375
  this.trackBitrates.push(info);
20753
21376
  }
21377
+ /**
21378
+ * Record how long this peer connection took to set up. Called once, when the initial connect
21379
+ * succeeds; resumes and ICE restarts keep the estimator they have and never call this.
21380
+ */
21381
+ setConnectionSetupTime(ms) {
21382
+ this.connectionSetupTimeMs = ms;
21383
+ }
21384
+ /** Record when the connection attempt that built this peer connection began. */
21385
+ setConnectionStartedAt(startedAt) {
21386
+ this.connectionStartedAt = startedAt;
21387
+ }
21388
+ /** The measured setup time, or while still connecting, the time elapsed so far. */
21389
+ connectionSetupTimeEstimateMs() {
21390
+ if (this.connectionSetupTimeMs !== undefined) {
21391
+ return this.connectionSetupTimeMs;
21392
+ }
21393
+ if (this.connectionStartedAt === undefined) {
21394
+ return undefined;
21395
+ }
21396
+ return Math.round(performance.now() - this.connectionStartedAt);
21397
+ }
20754
21398
  setConfiguration(rtcConfig) {
20755
21399
  var _a;
20756
21400
  if (!this._pc) {
@@ -21096,6 +21740,19 @@ function conformBundledCodecFmtp(media, isPlaceholder) {
21096
21740
  }
21097
21741
  }
21098
21742
  /** @internal */
21743
+ /**
21744
+ * Whether an incoming answer repeats one already applied or being applied.
21745
+ * Any answer for the offer whose answer is in flight is a duplicate, numbered
21746
+ * or not. With an offerId (current servers) an answer is also a duplicate if
21747
+ * that offer's answer was already applied. Without one, fall back to the
21748
+ * signaling state: in `stable` no local offer is pending, unless the initial
21749
+ * offer is still deferred (it is set locally only once its answer arrives).
21750
+ */
21751
+ function isDuplicateAnswer(offerId, answerInFlight, answeredOfferId, signalingState, hasPendingInitialOffer) {
21752
+ if (answerInFlight === offerId) return true;
21753
+ if (offerId > 0) return offerId === answeredOfferId;
21754
+ return signalingState === 'stable' && !hasPendingInitialOffer;
21755
+ }
21099
21756
  function extractStereoAndNackAudioFromOffer(offer) {
21100
21757
  var _a;
21101
21758
  const stereoMids = [];
@@ -21187,6 +21844,7 @@ const roomOptionDefaults = {
21187
21844
  };
21188
21845
  const roomConnectOptionDefaults = {
21189
21846
  autoSubscribe: true,
21847
+ disableIceLite: false,
21190
21848
  maxRetries: 1,
21191
21849
  peerConnectionTimeout: 15000,
21192
21850
  websocketTimeout: 15000
@@ -21400,7 +22058,9 @@ class PCTransportManager {
21400
22058
  try {
21401
22059
  if (this.isPublisherConnectionRequired && this.publisher.getConnectionState() !== 'connected' && this.publisher.getConnectionState() !== 'connecting') {
21402
22060
  this.log.debug('negotiation required, start negotiating');
21403
- this.publisher.negotiate();
22061
+ this.publisher.negotiate().catch(err => {
22062
+ this.log.error(err);
22063
+ });
21404
22064
  }
21405
22065
  yield Promise.all((_a = this.requiredTransports) === null || _a === void 0 ? void 0 : _a.map(transport => this.ensureTransportConnected(transport, abortController, timeout)));
21406
22066
  } finally {
@@ -21551,7 +22211,8 @@ class PCTransportManager {
21551
22211
  * Waiters are parked on the channel object captured at wait entry. If that object stops being
21552
22212
  * current — replaced or torn down — its events may never fire again, so the owner must call
21553
22213
  * {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
21554
- * gets a fresh controller.
22214
+ * gets a fresh controller. {@link attach} and {@link detach} do this, and close the released
22215
+ * handle, as part of handle turnover.
21555
22216
  */
21556
22217
  class FlowControlledDataChannel {
21557
22218
  constructor(opts) {
@@ -21571,25 +22232,38 @@ class FlowControlledDataChannel {
21571
22232
  return this.handle;
21572
22233
  }
21573
22234
  /**
21574
- * Attaches the channel handle this wrapper controls. Replacing an existing handle rejects
21575
- * parked waiters — their events would never fire again on the abandoned object — and installs a
21576
- * fresh controller, so queued senders re-check against the new channel. Wrappers outlive their
21577
- * handles: this is the one place handle turnover happens, which is what makes stranding a
21578
- * waiter structurally impossible.
22235
+ * Attaches the channel handle this wrapper controls. Replacing an existing handle closes it and
22236
+ * rejects parked waiters — their events would never fire again on the abandoned object — and
22237
+ * installs a fresh controller. Wrappers outlive their handles: this is the one place handle
22238
+ * turnover happens, so a handle the wrapper lets go of is never left open, and stranding a
22239
+ * waiter is structurally impossible.
21579
22240
  */
21580
22241
  attach(dc) {
21581
- if (this.handle && this.handle !== dc) {
21582
- this.invalidateWaiters('data channel replaced');
21583
- }
22242
+ const old = this.handle;
21584
22243
  this.handle = dc;
22244
+ if (old && old !== dc) {
22245
+ this.release(old, 'data channel replaced');
22246
+ }
21585
22247
  }
21586
- /** Detaches the handle on teardown, rejecting parked waiters. */
22248
+ /** Detaches and closes the handle on teardown, rejecting parked waiters. */
21587
22249
  detach() {
21588
22250
  let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'data channel torn down';
21589
- if (this.handle) {
21590
- this.invalidateWaiters(reason);
21591
- }
22251
+ const old = this.handle;
21592
22252
  this.handle = undefined;
22253
+ if (old) {
22254
+ this.release(old, reason);
22255
+ }
22256
+ }
22257
+ /**
22258
+ * Closes a handle the wrapper no longer controls. `close()` moves it to `closing` synchronously,
22259
+ * so senders still holding it (queued on the headroom lock) fail the open check once they get
22260
+ * the lock. Parked waiters are aborted directly instead of relying on the `close` event, which
22261
+ * fires asynchronously, if at all (the spec lets `pc.close()` skip it). Strip any handlers that
22262
+ * must not observe the close before calling this.
22263
+ */
22264
+ release(dc, reason) {
22265
+ this.invalidateWaiters(reason);
22266
+ dc.close();
21593
22267
  }
21594
22268
  getChannel() {
21595
22269
  return this.handle;
@@ -21620,32 +22294,35 @@ class FlowControlledDataChannel {
21620
22294
  return this.headroomLock.lock();
21621
22295
  }
21622
22296
  /**
21623
- * Resolves once the caller may send on this channel: immediately while the send buffer is at or
21624
- * below its high-water mark, otherwise once the buffer has drained to the low-water mark (the
22297
+ * Resolves once the caller may send on `dc`: immediately while its send buffer is at or below
22298
+ * the high-water mark, otherwise once the buffer has drained to the low-water mark (the
21625
22299
  * `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
21626
22300
  * the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
21627
22301
  * sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
21628
- * closed/buffer checks run inside the lock so queued callers proceed in FIFO order.
22302
+ * checks run inside the lock so queued callers proceed in FIFO order.
22303
+ *
22304
+ * The caller passes the handle it resolved before queueing, and the wait is against that one
22305
+ * handle throughout: if it was replaced meanwhile, it has been closed, so the wait rejects
22306
+ * rather than letting the caller send on the replacement out of turn.
21629
22307
  */
21630
- waitForHeadroomWithLock() {
22308
+ waitForHeadroomWithLock(dc) {
21631
22309
  return __awaiter(this, void 0, void 0, function* () {
21632
22310
  const unlock = yield this.lockHeadroom();
21633
22311
  try {
21634
- yield this.waitForHeadroomWithoutLock();
22312
+ yield this.waitForHeadroomWithoutLock(dc);
21635
22313
  } finally {
21636
22314
  unlock();
21637
22315
  }
21638
22316
  });
21639
22317
  }
21640
22318
  /** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
21641
- waitForHeadroomWithoutLock() {
22319
+ waitForHeadroomWithoutLock(dc) {
21642
22320
  return __awaiter(this, void 0, void 0, function* () {
21643
22321
  if (this.isEngineClosed()) {
21644
22322
  throw new UnexpectedConnectionState('engine closed');
21645
22323
  }
21646
- const dc = this.getChannel();
21647
- if (!dc) {
21648
- throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
22324
+ if (dc.readyState !== 'open') {
22325
+ throw new UnexpectedConnectionState("DataChannel ".concat(this.kind, " is ").concat(dc.readyState));
21649
22326
  }
21650
22327
  if (this.isBelowHighWaterMark(dc)) {
21651
22328
  return;
@@ -21735,7 +22412,16 @@ class LossyDataChannel extends FlowControlledDataChannel {
21735
22412
  switch (this.bufferFullBehavior) {
21736
22413
  case 'wait':
21737
22414
  if (!this.isBelowHighWaterMark(dc)) {
21738
- yield this.waitForHeadroomWithLock();
22415
+ try {
22416
+ yield this.waitForHeadroomWithLock(dc);
22417
+ } catch (error) {
22418
+ if (this.isEngineClosed()) {
22419
+ throw error;
22420
+ }
22421
+ // The channel closed or was replaced while this send waited. The replacement may
22422
+ // still be connecting, so drop the packet, as sends during a reconnect are.
22423
+ return;
22424
+ }
21739
22425
  }
21740
22426
  break;
21741
22427
  case 'drop':
@@ -21917,7 +22603,7 @@ class ReliableDataChannel extends FlowControlledDataChannel {
21917
22603
  return;
21918
22604
  }
21919
22605
  try {
21920
- yield this.waitForHeadroomWithLock();
22606
+ yield this.waitForHeadroomWithLock(dc);
21921
22607
  } catch (error) {
21922
22608
  if (this.isEngineClosed()) {
21923
22609
  // No replay is coming after an engine close — surface the failure.
@@ -21925,7 +22611,9 @@ class ReliableDataChannel extends FlowControlledDataChannel {
21925
22611
  }
21926
22612
  // Transient teardown (the channel closed or was replaced while we waited): the reliable
21927
22613
  // channel promises delivery across resume, so queue the packet for the replay instead of
21928
- // rejecting a send the app can't meaningfully retry.
22614
+ // rejecting a send the app can't meaningfully retry. This also keeps the order: a send
22615
+ // queued behind one aborted by a replacement fails the same way (the old handle is closed),
22616
+ // so it can't reach the replacement ahead of the lower sequence the replay will deliver.
21929
22617
  this.messageBuffer.push({
21930
22618
  data: msg,
21931
22619
  sequence,
@@ -21960,13 +22648,18 @@ class ReliableDataChannel extends FlowControlledDataChannel {
21960
22648
  */
21961
22649
  replay(lastMessageSeq) {
21962
22650
  return __awaiter(this, void 0, void 0, function* () {
21963
- const dc = this.getChannel();
21964
- if (!dc) {
22651
+ if (!this.getChannel()) {
21965
22652
  return;
21966
22653
  }
21967
22654
  this.messageBuffer.popToSequence(lastMessageSeq);
21968
22655
  const unlock = yield this.lockHeadroom();
21969
22656
  try {
22657
+ // Resolved only once the lock is held: the handle can be replaced while the replay waits
22658
+ // for it.
22659
+ const dc = this.getChannel();
22660
+ if (!dc) {
22661
+ throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
22662
+ }
21970
22663
  // Everything left after the ack cutoff must be re-handed to the current channel.
21971
22664
  this.messageBuffer.markAllUnsent();
21972
22665
  // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
@@ -21978,7 +22671,10 @@ class ReliableDataChannel extends FlowControlledDataChannel {
21978
22671
  for (let batch = this.messageBuffer.getUnsent(); batch.length > 0; batch = this.messageBuffer.getUnsent()) {
21979
22672
  for (const item of batch) {
21980
22673
  // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
21981
- yield this.waitForHeadroomWithoutLock();
22674
+ // Against the handle resolved under the lock: if it is replaced mid-replay (another
22675
+ // resume), it has been closed, so this throws and that resume's replay picks up the
22676
+ // entries still flagged unsent.
22677
+ yield this.waitForHeadroomWithoutLock(dc);
21982
22678
  dc.send(item.data);
21983
22679
  this.messageBuffer.markSent(item);
21984
22680
  }
@@ -22033,13 +22729,23 @@ function dataChannelHighWaterMark(kind) {
22033
22729
  }const lossyDataChannelLabel = '_lossy';
22034
22730
  const reliableDataChannelLabel = '_reliable';
22035
22731
  const dataTrackDataChannelLabel = '_data_track';
22732
+ /** Unhooks every handler the manager may have wired onto `dc`, ahead of closing it. */
22733
+ function clearHandlers(dc) {
22734
+ dc.onbufferedamountlow = null;
22735
+ dc.onclose = null;
22736
+ dc.onclosing = null;
22737
+ dc.onerror = null;
22738
+ dc.onmessage = null;
22739
+ dc.onopen = null;
22740
+ }
22036
22741
  /**
22037
22742
  * Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
22038
22743
  * live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
22039
22744
  * connections come and go) plus the subscriber-side receive handles adopted by label.
22040
22745
  *
22041
- * Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which reject
22042
- * parked headroom waiters as a built-in — there is no separate invalidation step to forget.
22746
+ * Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which close the
22747
+ * released handle and reject parked headroom waiters as a built-in — there is no separate
22748
+ * invalidation step to forget.
22043
22749
  */
22044
22750
  class DataChannelManager {
22045
22751
  constructor(opts) {
@@ -22097,17 +22803,16 @@ class DataChannelManager {
22097
22803
  }
22098
22804
  /**
22099
22805
  * Creates the three publisher data channels on the given transport, wires their handlers, and
22100
- * attaches them to the wrappers — attaching rejects any waiters still parked on replaced
22101
- * channel objects.
22806
+ * attaches them to the wrappers — attaching closes any replaced channel objects and rejects the
22807
+ * waiters still parked on them.
22102
22808
  */
22103
22809
  createPublisherChannels(pcManager) {
22104
- // clear old data channel callbacks if recreate
22810
+ // Recreating (the Safari null-id path): attaching the new handles below closes the old ones,
22811
+ // so unhook them first — the old channel's close must not report an unexpected close.
22105
22812
  for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
22106
22813
  const old = channel.channelHandle;
22107
22814
  if (old) {
22108
- old.onmessage = null;
22109
- old.onerror = null;
22110
- old.onclose = null;
22815
+ clearHandlers(old);
22111
22816
  }
22112
22817
  }
22113
22818
  const wire = (channel, dc, onMessage) => {
@@ -22158,38 +22863,35 @@ class DataChannelManager {
22158
22863
  return true;
22159
22864
  }
22160
22865
  /**
22161
- * Tears down all channels for a peer-connection cleanup: rejects parked waiters (detach — the
22162
- * spec allows `pc.close()` to transition channels to 'closed' without firing events, so waiting
22163
- * for browser close events is not an option), strips handlers, closes the handles, and resets
22164
- * the reliable session state.
22866
+ * Tears down all channels for a peer-connection cleanup: strips handlers, closes the handles,
22867
+ * and resets the reliable session state. Detaching the publisher handles closes them and
22868
+ * rejects parked waiters (the spec allows `pc.close()` to transition channels to 'closed'
22869
+ * without firing events, so waiting for browser close events is not an option).
22165
22870
  */
22166
22871
  teardown() {
22167
- const dcCleanup = dc => {
22168
- if (!dc) {
22169
- return;
22170
- }
22171
- // Detach the data channel handlers before closing anything. Closing a peer connection tears
22172
- // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
22173
- // channels; if our handlers are still attached at that point, handleDataError logs a spurious
22174
- // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
22175
- // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
22176
- // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
22177
- dc.onbufferedamountlow = null;
22178
- dc.onclose = null;
22179
- dc.onclosing = null;
22180
- dc.onerror = null;
22181
- dc.onmessage = null;
22182
- dc.onopen = null;
22183
- dc.close();
22184
- };
22185
- for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
22872
+ // Detach the data channel handlers before closing anything. Closing a peer connection tears
22873
+ // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
22874
+ // channels; if our handlers are still attached at that point, handleDataError logs a spurious
22875
+ // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
22876
+ // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
22877
+ // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
22878
+ const publisherDataChannels = [this.lossy, this.reliable, this.dataTrack];
22879
+ const subscriberDataChannels = [this.lossySub, this.reliableSub, this.dataTrackSub];
22880
+ // Publisher handles are closed by detach, which also rejects parked waiters.
22881
+ for (const channel of publisherDataChannels) {
22186
22882
  const dc = channel.channelHandle;
22883
+ if (dc) {
22884
+ clearHandlers(dc);
22885
+ }
22187
22886
  channel.detach('peer connections cleaned up');
22188
- dcCleanup(dc);
22189
22887
  }
22190
- dcCleanup(this.lossySub);
22191
- dcCleanup(this.reliableSub);
22192
- dcCleanup(this.dataTrackSub);
22888
+ // Subscriber handles have no wrapper, so close them directly.
22889
+ for (const dc of subscriberDataChannels) {
22890
+ if (dc) {
22891
+ clearHandlers(dc);
22892
+ dc.close();
22893
+ }
22894
+ }
22193
22895
  this.lossySub = undefined;
22194
22896
  this.reliableSub = undefined;
22195
22897
  this.dataTrackSub = undefined;
@@ -22351,6 +23053,9 @@ class LocalTrack extends Track {
22351
23053
  get isUserProvided() {
22352
23054
  return this.providedByUser;
22353
23055
  }
23056
+ get isStopped() {
23057
+ return this.manuallyStopped;
23058
+ }
22354
23059
  get mediaStreamTrack() {
22355
23060
  var _a, _b;
22356
23061
  return (_b = (_a = this.processor) === null || _a === void 0 ? void 0 : _a.processedTrack) !== null && _b !== void 0 ? _b : this._mediaStreamTrack;
@@ -22408,6 +23113,9 @@ class LocalTrack extends Track {
22408
23113
  track: newTrack,
22409
23114
  kind: this.kind,
22410
23115
  element: this.processorElement,
23116
+ // audio processors need the context they were initialised with in order to rebuild
23117
+ // their graph, omitting it here would leave them without one
23118
+ audioContext: this.audioContext,
22411
23119
  localTrack: this
22412
23120
  });
22413
23121
  processedTrack = this.processor.processedTrack;
@@ -22431,6 +23139,18 @@ class LocalTrack extends Track {
22431
23139
  attachToElement(processedTrack !== null && processedTrack !== void 0 ? processedTrack : newTrack, el);
22432
23140
  });
22433
23141
  }
23142
+ yield this.onMediaStreamTrackChanged();
23143
+ });
23144
+ }
23145
+ /**
23146
+ * Hook invoked after the underlying MediaStreamTrack has been swapped and the processor has
23147
+ * been restarted around it, so `processor.processedTrack` already holds the new track.
23148
+ * Unlike `onSenderTrackSwapped` this also fires on the restart and unmute paths.
23149
+ */
23150
+ onMediaStreamTrackChanged() {
23151
+ return __awaiter(this, void 0, void 0, function* () {
23152
+ // base implementation is a no-op; LocalAudioTrack overrides this to move the krisp
23153
+ // feature listeners onto the new processed track.
22434
23154
  });
22435
23155
  }
22436
23156
  waitForDimensions() {
@@ -22438,21 +23158,29 @@ class LocalTrack extends Track {
22438
23158
  var _this = this;
22439
23159
  let timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_DIMENSIONS_TIMEOUT;
22440
23160
  return function* () {
22441
- var _a;
22442
23161
  if (_this.kind === Track.Kind.Audio) {
22443
23162
  throw new Error('cannot get dimensions for audio tracks');
22444
23163
  }
22445
- if (((_a = getBrowser()) === null || _a === void 0 ? void 0 : _a.os) === 'iOS') {
22446
- // browsers report wrong initial resolution on iOS.
22447
- // when slightly delaying the call to .getSettings(), the correct resolution is being reported
22448
- yield sleep(10);
22449
- }
22450
23164
  const started = Date.now();
22451
- while (Date.now() - started < timeout) {
23165
+ if (isAppleMobile()) {
23166
+ // iOS and iPadOS keep reporting the camera's sensor frame from getSettings() until the
23167
+ // camera has delivered its first picture, so a portrait capture initially reads as
23168
+ // landscape. Wait for that first frame instead of guessing at a delay.
23169
+ // Gated to the Apple capture pipeline rather than run everywhere: elsewhere getSettings() is
23170
+ // already right and the first frame can be hundreds of milliseconds out (measured ~500ms on
23171
+ // macOS Safari), which would be pure added publish latency.
23172
+ // https://github.com/livekit/client-sdk-js/issues/2099
23173
+ // https://github.com/livekit/client-sdk-js/issues/2107
23174
+ yield waitForFirstVideoFrame(_this._mediaStreamTrack, timeout, _this.attachedElements);
23175
+ }
23176
+ while (true) {
22452
23177
  const dims = _this.dimensions;
22453
23178
  if (dims) {
22454
23179
  return dims;
22455
23180
  }
23181
+ if (Date.now() - started >= timeout) {
23182
+ break;
23183
+ }
22456
23184
  yield sleep(50);
22457
23185
  }
22458
23186
  throw new TrackInvalidError('unable to get track dimensions after timeout');
@@ -22461,7 +23189,7 @@ class LocalTrack extends Track {
22461
23189
  }
22462
23190
  setDeviceId(deviceId) {
22463
23191
  return __awaiter(this, void 0, void 0, function* () {
22464
- if (this._constraints.deviceId === deviceId && this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)) {
23192
+ if (this._constraints.deviceId === deviceId && this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId) && this._mediaStreamTrack.readyState === 'live') {
22465
23193
  return true;
22466
23194
  }
22467
23195
  this._constraints.deviceId = deviceId;
@@ -22589,24 +23317,40 @@ class LocalTrack extends Track {
22589
23317
  // 'A MediaStreamTrack ended due to a capture failure`
22590
23318
  this._mediaStreamTrack.stop();
22591
23319
  // create new track and attach
22592
- const mediaStream = yield navigator.mediaDevices.getUserMedia(streamConstraints);
23320
+ let mediaStream;
23321
+ try {
23322
+ mediaStream = yield navigator.mediaDevices.getUserMedia(streamConstraints);
23323
+ } catch (e) {
23324
+ markDeviceAcquisitionFailure(e);
23325
+ throw e;
23326
+ }
22593
23327
  const newTrack = mediaStream.getTracks()[0];
22594
- if (this.kind === Track.Kind.Video) {
22595
- // we already captured the audio track with the constraints, so we only need to apply the video constraints
22596
- yield newTrack.applyConstraints(otherConstraints);
23328
+ try {
23329
+ if (this.kind === Track.Kind.Video) {
23330
+ // we already captured the audio track with the constraints, so we only need to apply the video constraints
23331
+ yield newTrack.applyConstraints(otherConstraints);
23332
+ }
23333
+ newTrack.addEventListener('ended', this.handleEnded);
23334
+ this.log.debug('re-acquired MediaStreamTrack', this.logContext);
23335
+ yield this.setMediaStreamTrack(newTrack, false, isUnmuting);
23336
+ } catch (e) {
23337
+ // setMediaStreamTrack can throw after acquisition but before it adopts the new track,
23338
+ // which would leave the capture device live with nothing referencing it
23339
+ if (this._mediaStreamTrack !== newTrack) {
23340
+ newTrack.stop();
23341
+ }
23342
+ throw e;
22597
23343
  }
22598
- newTrack.addEventListener('ended', this.handleEnded);
22599
- this.log.debug('re-acquired MediaStreamTrack', this.logContext);
22600
- yield this.setMediaStreamTrack(newTrack, false, isUnmuting);
22601
23344
  this._constraints = constraints;
22602
23345
  this.pendingDeviceChange = false;
22603
23346
  this.emit(TrackEvent.Restarted, this);
23347
+ return this;
23348
+ } finally {
23349
+ // in the finally because a throw can also leave the new track adopted, and so still live
22604
23350
  if (this.manuallyStopped) {
22605
23351
  this.log.warn('track was stopped during a restart, stopping restarted track', this.logContext);
22606
23352
  this.stop();
22607
23353
  }
22608
- return this;
22609
- } finally {
22610
23354
  unlock();
22611
23355
  }
22612
23356
  });
@@ -22647,7 +23391,13 @@ class LocalTrack extends Track {
22647
23391
  this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
22648
23392
  this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
22649
23393
  this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
22650
- (_a = this.processor) === null || _a === void 0 ? void 0 : _a.destroy();
23394
+ // `stop` is synchronous, so we can't await the teardown of the processor here.
23395
+ // make sure a failing teardown doesn't surface as an unhandled rejection.
23396
+ (_a = this.processor) === null || _a === void 0 ? void 0 : _a.destroy().catch(error => {
23397
+ this.log.error('failed to destroy processor', Object.assign(Object.assign({}, this.logContext), {
23398
+ error
23399
+ }));
23400
+ });
22651
23401
  this.processor = undefined;
22652
23402
  }
22653
23403
  /**
@@ -22726,6 +23476,11 @@ class LocalTrack extends Track {
22726
23476
  * Sets a processor on this track.
22727
23477
  * See https://github.com/livekit/track-processors-js for example usage
22728
23478
  *
23479
+ * The processor lives on the track, so stopping the track destroys it. Unpublishing and
23480
+ * disconnecting both stop the track by default, which means a processor has to be set again on
23481
+ * the track published after a reconnect. See {@link TrackProcessor} for the full lifecycle,
23482
+ * including what `stopLocalTrackOnUnpublish: false` changes.
23483
+ *
22729
23484
  * @param processor
22730
23485
  * @param showProcessedStreamLocally
22731
23486
  * @returns
@@ -22795,6 +23550,10 @@ class LocalTrack extends Track {
22795
23550
  }();
22796
23551
  });
22797
23552
  }
23553
+ /**
23554
+ * Returns the processor currently set on this track, or `undefined` if there is none. A track
23555
+ * that was stopped by an unpublish or a disconnect has none. See {@link TrackProcessor}.
23556
+ */
22798
23557
  getProcessor() {
22799
23558
  return this.processor;
22800
23559
  }
@@ -22802,6 +23561,8 @@ class LocalTrack extends Track {
22802
23561
  * Stops the track processor
22803
23562
  * See https://github.com/livekit/track-processors-js for example usage
22804
23563
  *
23564
+ * Only needed to remove a processor from a track that stays published. Unpublishing or
23565
+ * disconnecting already destroys it. See {@link TrackProcessor}.
22805
23566
  */
22806
23567
  stopProcessor() {
22807
23568
  return __awaiter(this, arguments, void 0, function () {
@@ -23042,6 +23803,38 @@ class LocalTrack extends Track {
23042
23803
  this.monitorSender();
23043
23804
  }, monitorFrequency);
23044
23805
  }
23806
+ /**
23807
+ * registers the krisp feature listeners on the given processed track, moving them over from
23808
+ * a previously observed one. Passing `undefined` just removes the existing listeners.
23809
+ */
23810
+ observeProcessedTrack(processedTrack) {
23811
+ if (this.observedProcessedTrack === processedTrack) {
23812
+ return;
23813
+ }
23814
+ if (this.observedProcessedTrack) {
23815
+ this.observedProcessedTrack.removeEventListener('enable-lk-krisp-noise-filter', this.handleKrispNoiseFilterEnable);
23816
+ this.observedProcessedTrack.removeEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
23817
+ this.observedProcessedTrack = undefined;
23818
+ if (this.isKrispNoiseFilterEnabled) {
23819
+ // we're no longer observing the track that had the filter enabled, so the feature
23820
+ // can't be active anymore. Make sure the state doesn't get stuck on `true`.
23821
+ this.handleKrispNoiseFilterDisable();
23822
+ }
23823
+ }
23824
+ if (!processedTrack) {
23825
+ return;
23826
+ }
23827
+ processedTrack.addEventListener('enable-lk-krisp-noise-filter', this.handleKrispNoiseFilterEnable);
23828
+ processedTrack.addEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
23829
+ this.observedProcessedTrack = processedTrack;
23830
+ }
23831
+ /**
23832
+ * Sets a processor on this track.
23833
+ *
23834
+ * The processor is destroyed when the track is stopped, and also when the audio context goes
23835
+ * away, so a disconnect ends it on both counts and it has to be set again after reconnecting.
23836
+ * See {@link TrackProcessor} for the full lifecycle.
23837
+ */
23045
23838
  setProcessor(processor) {
23046
23839
  return __awaiter(this, void 0, void 0, function* () {
23047
23840
  var _a;
@@ -23065,8 +23858,7 @@ class LocalTrack extends Track {
23065
23858
  this.processor = processor;
23066
23859
  if (this.processor.processedTrack) {
23067
23860
  yield (_a = this.sender) === null || _a === void 0 ? void 0 : _a.replaceTrack(this.processor.processedTrack);
23068
- this.processor.processedTrack.addEventListener('enable-lk-krisp-noise-filter', this.handleKrispNoiseFilterEnable);
23069
- this.processor.processedTrack.addEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
23861
+ this.observeProcessedTrack(this.processor.processedTrack);
23070
23862
  }
23071
23863
  this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
23072
23864
  } finally {
@@ -23074,12 +23866,73 @@ class LocalTrack extends Track {
23074
23866
  }
23075
23867
  });
23076
23868
  }
23869
+ onMediaStreamTrackChanged() {
23870
+ return __awaiter(this, void 0, void 0, function* () {
23871
+ var _a;
23872
+ // the processor rebuilds its graph around the new track, so the krisp listeners have to
23873
+ // follow it onto whatever it hands back
23874
+ this.observeProcessedTrack((_a = this.processor) === null || _a === void 0 ? void 0 : _a.processedTrack);
23875
+ });
23876
+ }
23877
+ internalStopProcessor() {
23878
+ const _super = Object.create(null, {
23879
+ internalStopProcessor: {
23880
+ get: () => super.internalStopProcessor
23881
+ }
23882
+ });
23883
+ return __awaiter(this, arguments, void 0, function () {
23884
+ var _this = this;
23885
+ let keepElement = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
23886
+ return function* () {
23887
+ _this.observeProcessedTrack(undefined);
23888
+ yield _super.internalStopProcessor.call(_this, keepElement);
23889
+ }();
23890
+ });
23891
+ }
23892
+ stop() {
23893
+ this.observeProcessedTrack(undefined);
23894
+ super.stop();
23895
+ }
23077
23896
  /**
23078
23897
  * @internal
23079
23898
  * @experimental
23899
+ *
23900
+ * A processor's nodes (`AudioWorkletNode`, ...) can't move between contexts, so the processor
23901
+ * is rebuilt whenever the context changes and torn down when there is none left to run in.
23080
23902
  */
23081
23903
  setAudioContext(audioContext) {
23082
- this.audioContext = audioContext;
23904
+ return __awaiter(this, void 0, void 0, function* () {
23905
+ var _a;
23906
+ if (this.audioContext === audioContext) {
23907
+ return;
23908
+ }
23909
+ const unlock = yield this.trackChangeLock.lock();
23910
+ try {
23911
+ this.audioContext = audioContext;
23912
+ if (!this.processor || isReactNative()) {
23913
+ return;
23914
+ }
23915
+ if (!audioContext) {
23916
+ this.log.debug('stopping audio processor, audio context has been removed', this.logContext);
23917
+ yield this.internalStopProcessor();
23918
+ return;
23919
+ }
23920
+ this.log.debug('restarting audio processor on new audio context', this.logContext);
23921
+ yield this.processor.restart({
23922
+ kind: this.kind,
23923
+ track: this._mediaStreamTrack,
23924
+ audioContext,
23925
+ localTrack: this
23926
+ });
23927
+ if (this.processor.processedTrack) {
23928
+ yield (_a = this.sender) === null || _a === void 0 ? void 0 : _a.replaceTrack(this.processor.processedTrack);
23929
+ this.observeProcessedTrack(this.processor.processedTrack);
23930
+ }
23931
+ this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
23932
+ } finally {
23933
+ unlock();
23934
+ }
23935
+ });
23083
23936
  }
23084
23937
  getSenderStats() {
23085
23938
  return __awaiter(this, void 0, void 0, function* () {
@@ -23155,24 +24008,6 @@ const computeDefaultScreenShareSimulcastPresets = fromPreset => {
23155
24008
  return new VideoPreset(Math.floor(fromPreset.width / t.scaleResolutionDownBy), Math.floor(fromPreset.height / t.scaleResolutionDownBy), Math.max(150000, Math.floor(fromPreset.encoding.maxBitrate / (Math.pow(t.scaleResolutionDownBy, 2) * (((_a = fromPreset.encoding.maxFramerate) !== null && _a !== void 0 ? _a : 30) / ((_b = t.fps) !== null && _b !== void 0 ? _b : 30))))), t.fps, fromPreset.encoding.priority);
23156
24009
  });
23157
24010
  };
23158
- // /**
23159
- // *
23160
- // * @internal
23161
- // * @experimental
23162
- // */
23163
- // const computeDefaultMultiCodecSimulcastEncodings = (width: number, height: number) => {
23164
- // // use vp8 as a default
23165
- // const vp8 = determineAppropriateEncoding(false, width, height);
23166
- // const vp9 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.9 };
23167
- // const h264 = { ...vp8, maxBitrate: vp8.maxBitrate * 1.1 };
23168
- // const av1 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.7 };
23169
- // return {
23170
- // vp8,
23171
- // vp9,
23172
- // h264,
23173
- // av1,
23174
- // };
23175
- // };
23176
24011
  const videoRids = ['q', 'h', 'f'];
23177
24012
  /* @internal */
23178
24013
  function computeVideoEncodings(isScreenShare, width, height, options) {
@@ -23927,6 +24762,13 @@ class LocalVideoTrack extends LocalTrack {
23927
24762
  yield sender.setParameters(params);
23928
24763
  });
23929
24764
  }
24765
+ /**
24766
+ * Sets a processor on this track, and wires its processed track into the simulcast codecs.
24767
+ *
24768
+ * The processor is destroyed when the track is stopped, which unpublishing and disconnecting
24769
+ * both do by default, so it has to be set again after reconnecting. See {@link TrackProcessor}
24770
+ * for the full lifecycle.
24771
+ */
23930
24772
  setProcessor(processor_1) {
23931
24773
  const _super = Object.create(null, {
23932
24774
  setProcessor: {
@@ -24027,12 +24869,20 @@ class LocalVideoTrack extends LocalTrack {
24027
24869
  // browser will reenable disabled codec/layers after new codec has been published,
24028
24870
  // so refresh subscribedCodecs after publish a new codec
24029
24871
  setTimeout(() => {
24030
- if (this.subscribedCodecs) {
24031
- this.setPublishingCodecs(this.subscribedCodecs);
24032
- }
24872
+ this.refreshSubscribedCodecs();
24033
24873
  }, refreshSubscribedCodecAfterNewCodec);
24034
24874
  });
24035
24875
  }
24876
+ /** @interal */
24877
+ refreshSubscribedCodecs() {
24878
+ return __awaiter(this, void 0, void 0, function* () {
24879
+ if (this.subscribedCodecs) {
24880
+ return this.setPublishingCodecs(this.subscribedCodecs).catch(e => this.log.warn("failed to refresh codec for track", Object.assign(Object.assign({}, this.logContext), {
24881
+ error: e
24882
+ })));
24883
+ }
24884
+ });
24885
+ }
24036
24886
  /**
24037
24887
  * @internal
24038
24888
  * Sets codecs that should be publishing, returns new codecs that have not yet
@@ -24377,6 +25227,13 @@ class RTCEngine extends eventsExports.EventEmitter {
24377
25227
  this.midToTrackId = {};
24378
25228
  /** used to indicate whether the browser is currently waiting to reconnect */
24379
25229
  this.isWaitingForNetworkReconnect = false;
25230
+ /**
25231
+ * Abort handlers for the in-flight `negotiate()` calls, which a single central
25232
+ * Closing/Restarting listener pair fans out to. Registering a listener pair per call instead
25233
+ * accumulates them on the engine emitter and trips its max-listener warning under
25234
+ * renegotiation bursts.
25235
+ */
25236
+ this.pendingNegotiationAborts = new Set();
24380
25237
  this.handleDataChannel = _a => __awaiter(this, [_a], void 0, function (_ref) {
24381
25238
  var _this = this;
24382
25239
  let channel = _ref.channel;
@@ -24513,7 +25370,17 @@ class RTCEngine extends eventsExports.EventEmitter {
24513
25370
  return;
24514
25371
  }
24515
25372
  if (connection === leaveReconnect) {
24516
- delay = 0;
25373
+ // The server asked for a reconnect, so skip any remaining backoff. Use the policy's first
25374
+ // retry delay instead of 0 so that clients told to reconnect together don't all reconnect
25375
+ // at the same instant.
25376
+ delay = this.getNextRetryDelay({
25377
+ elapsedMs: duration,
25378
+ retryCount: 0
25379
+ });
25380
+ if (delay === null) {
25381
+ disconnect(duration);
25382
+ return;
25383
+ }
24517
25384
  }
24518
25385
  this.log.debug("reconnecting in ".concat(delay, "ms"));
24519
25386
  this.clearReconnectTimeout();
@@ -24622,6 +25489,14 @@ class RTCEngine extends eventsExports.EventEmitter {
24622
25489
  this.client.onRequestResponse = response => this.emit(EngineEvent.SignalRequestResponse, response);
24623
25490
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
24624
25491
  this.client.onJoined = joinResponse => this.emit(EngineEvent.Joined, joinResponse);
25492
+ const abortPendingNegotiations = () => {
25493
+ // Iterate a copy: each handler removes itself from the set as its negotiation settles.
25494
+ for (const abort of Array.from(this.pendingNegotiationAborts)) {
25495
+ abort();
25496
+ }
25497
+ };
25498
+ this.on(EngineEvent.Closing, abortPendingNegotiations);
25499
+ this.on(EngineEvent.Restarting, abortPendingNegotiations);
24625
25500
  }
24626
25501
  /** @internal */
24627
25502
  get logContext() {
@@ -24638,7 +25513,7 @@ class RTCEngine extends eventsExports.EventEmitter {
24638
25513
  var _this2 = this;
24639
25514
  let useV0Path = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
24640
25515
  return function* () {
24641
- var _a, _b, _c;
25516
+ var _a, _b, _c, _d;
24642
25517
  _this2._isNewlyCreated = false;
24643
25518
  _this2.url = url;
24644
25519
  _this2.token = token;
@@ -24646,6 +25521,7 @@ class RTCEngine extends eventsExports.EventEmitter {
24646
25521
  _this2.maxJoinAttempts = opts.maxRetries;
24647
25522
  try {
24648
25523
  _this2.joinAttempts += 1;
25524
+ _this2.joinStartedAt = performance.now();
24649
25525
  _this2.setupSignalClientCallbacks();
24650
25526
  // Whether the initial publisher offer is bundled with the join request. Computed once and
24651
25527
  // reused after the join below. Only the (non-Firefox) offer-with-join path does this.
@@ -24675,6 +25551,10 @@ class RTCEngine extends eventsExports.EventEmitter {
24675
25551
  throw ConnectionError.serviceNotFound('Simulated v1 path failure', 'v0-rtc');
24676
25552
  }
24677
25553
  const joinResponse = yield _this2.client.join(url, token, opts, abortSignal, useV0Path, offerProto);
25554
+ if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
25555
+ _this2.log.info('join cancelled after the signal join response');
25556
+ throw ConnectionError.cancelled('Connection aborted');
25557
+ }
24678
25558
  _this2._isClosed = false;
24679
25559
  _this2.latestJoinResponse = joinResponse;
24680
25560
  _this2.participantSid = (_b = joinResponse.participant) === null || _b === void 0 ? void 0 : _b.sid;
@@ -24687,6 +25567,10 @@ class RTCEngine extends eventsExports.EventEmitter {
24687
25567
  // the server's ICE servers and topology, then negotiate separately rather than bundling
24688
25568
  // the offer with the join.
24689
25569
  yield _this2.configure(joinResponse, !useV0Path);
25570
+ if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
25571
+ _this2.log.info('join cancelled after configuring peer connections');
25572
+ throw ConnectionError.cancelled('Connection aborted');
25573
+ }
24690
25574
  if (!useV0Path) {
24691
25575
  // The V1 first offer must carry the media layout so Firefox binds receive decoders for
24692
25576
  // subscribed tracks — without it, subscribed audio/video arrive as RTP but
@@ -24701,6 +25585,11 @@ class RTCEngine extends eventsExports.EventEmitter {
24701
25585
  });
24702
25586
  }
24703
25587
  }
25588
+ if (_this2.joinStartedAt !== undefined) {
25589
+ // Lets a video published before the transports connect be seeded from the time elapsed
25590
+ // so far, rather than delaying its offer until the setup time is measured.
25591
+ (_d = _this2.pcManager) === null || _d === void 0 ? void 0 : _d.publisher.setConnectionStartedAt(_this2.joinStartedAt);
25592
+ }
24704
25593
  _this2.registerOnLineListener();
24705
25594
  _this2.clientConfiguration = joinResponse.clientConfiguration;
24706
25595
  _this2.emit(EngineEvent.SignalConnected, joinResponse);
@@ -24749,6 +25638,9 @@ class RTCEngine extends eventsExports.EventEmitter {
24749
25638
  */
24750
25639
  close(reason) {
24751
25640
  return __awaiter(this, void 0, void 0, function* () {
25641
+ var _a;
25642
+ (_a = this.reconnectAbortController) === null || _a === void 0 ? void 0 : _a.abort();
25643
+ this.reconnectAbortController = undefined;
24752
25644
  const unlock = yield this.closingLock.lock();
24753
25645
  if (this.isClosed) {
24754
25646
  unlock();
@@ -24757,6 +25649,7 @@ class RTCEngine extends eventsExports.EventEmitter {
24757
25649
  try {
24758
25650
  this._isClosed = true;
24759
25651
  this.joinAttempts = 0;
25652
+ this.joinStartedAt = undefined;
24760
25653
  this.emit(EngineEvent.Closing);
24761
25654
  this.removeAllListeners();
24762
25655
  this.deregisterOnLineListener();
@@ -24885,6 +25778,7 @@ class RTCEngine extends eventsExports.EventEmitter {
24885
25778
  };
24886
25779
  this.pcManager.onDataChannel = this.handleDataChannel;
24887
25780
  this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
25781
+ var _a;
24888
25782
  this.log.debug("primary PC state changed ".concat(connectionState));
24889
25783
  // Record when the primary transport actually entered CONNECTING so
24890
25784
  // verifyTransport() can bound how long we tolerate it. Deriving it from the
@@ -24900,6 +25794,16 @@ class RTCEngine extends eventsExports.EventEmitter {
24900
25794
  this.publisherConnectionPromise = undefined;
24901
25795
  }
24902
25796
  if (connectionState === PCTransportState.CONNECTED) {
25797
+ if (this.joinStartedAt !== undefined) {
25798
+ // Connection setup time is the only network signal there is before the first video
25799
+ // offer. Set synchronously, so it is in place before `room.connect()` resolves and the
25800
+ // app can publish. A full restart joins again and builds new transports, so it is
25801
+ // measured again; a resume keeps its estimator and never restarts the timer.
25802
+ const setupTimeMs = Math.round(performance.now() - this.joinStartedAt);
25803
+ this.joinStartedAt = undefined;
25804
+ this.log.info("connection setup took ".concat(setupTimeMs, " ms"));
25805
+ (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.publisher.setConnectionSetupTime(setupTimeMs);
25806
+ }
24903
25807
  const shouldEmit = this.pcState === PCState.New;
24904
25808
  this.pcState = PCState.Connected;
24905
25809
  if (shouldEmit) {
@@ -24944,6 +25848,7 @@ class RTCEngine extends eventsExports.EventEmitter {
24944
25848
  this.midToTrackId = midToTrackId;
24945
25849
  }
24946
25850
  yield this.pcManager.setPublisherAnswer(sd, offerId);
25851
+ this.emit(EngineEvent.RequestSubscribedCodecRefresh);
24947
25852
  });
24948
25853
  // add candidate on trickle
24949
25854
  this.client.onTrickle = (candidate, target) => {
@@ -25010,7 +25915,9 @@ class RTCEngine extends eventsExports.EventEmitter {
25010
25915
  };
25011
25916
  this.client.onMediaSectionsRequirement = requirement => {
25012
25917
  this.addMediaSections(requirement.numAudios, requirement.numVideos);
25013
- this.negotiate();
25918
+ this.negotiate().catch(err => {
25919
+ this.log.error(err);
25920
+ });
25014
25921
  };
25015
25922
  this.client.onPublishDataTrackResponse = event => {
25016
25923
  this.emit(EngineEvent.PublishDataTrackResponse, event);
@@ -25131,6 +26038,12 @@ class RTCEngine extends eventsExports.EventEmitter {
25131
26038
  if (!this.pcManager) {
25132
26039
  return;
25133
26040
  }
26041
+ if (this.dataChannels.hasPublisherChannels) {
26042
+ // Replacing live channels (the Safari null-id path in resumeConnection): the new ones start
26043
+ // out connecting, so drop the memoized readiness check and make the next sender wait for
26044
+ // them to open.
26045
+ this.publisherConnectionPromise = undefined;
26046
+ }
25134
26047
  this.dataChannels.createPublisherChannels(this.pcManager);
25135
26048
  }
25136
26049
  /** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
@@ -25363,16 +26276,28 @@ class RTCEngine extends eventsExports.EventEmitter {
25363
26276
  const fullReconnect = this.fullReconnectOnNext;
25364
26277
  this.fullReconnectOnNext = false;
25365
26278
  let succeeded = false;
26279
+ const abortController = new AbortController();
26280
+ this.reconnectAbortController = abortController;
25366
26281
  try {
25367
26282
  this.attemptingReconnect = true;
25368
26283
  if (fullReconnect) {
25369
- yield this.restartConnection();
26284
+ yield this.restartConnection(abortController.signal);
25370
26285
  } else {
25371
26286
  yield this.resumeConnection(reason);
25372
26287
  }
26288
+ if (abortController.signal.aborted) {
26289
+ this.log.info('reconnect attempt cancelled, not marking it as succeeded');
26290
+ return;
26291
+ }
25373
26292
  this.clearPendingReconnect();
25374
26293
  succeeded = true;
25375
26294
  } catch (e) {
26295
+ if (abortController.signal.aborted) {
26296
+ this.log.info('reconnect attempt cancelled, not retrying', {
26297
+ error: e
26298
+ });
26299
+ return;
26300
+ }
25376
26301
  this.reconnectAttempts += 1;
25377
26302
  let recoverable = true;
25378
26303
  if (e instanceof UnexpectedConnectionState) {
@@ -25394,6 +26319,10 @@ class RTCEngine extends eventsExports.EventEmitter {
25394
26319
  yield this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms"));
25395
26320
  }
25396
26321
  } finally {
26322
+ // close() may already have cleared this (or a later attempt replaced it); only release our own
26323
+ if (this.reconnectAbortController === abortController) {
26324
+ this.reconnectAbortController = undefined;
26325
+ }
25397
26326
  this.attemptingReconnect = false;
25398
26327
  // A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
25399
26328
  // during a resume or a restart) that a successful attempt didn't act on; dispatch it now
@@ -25416,10 +26345,16 @@ class RTCEngine extends eventsExports.EventEmitter {
25416
26345
  // error in user code with provided reconnect policy, stop reconnecting
25417
26346
  return null;
25418
26347
  }
25419
- restartConnection(regionUrl) {
26348
+ restartConnection(abortSignal, regionUrl) {
25420
26349
  return __awaiter(this, void 0, void 0, function* () {
25421
26350
  var _a, _b, _c;
25422
26351
  try {
26352
+ if (abortSignal.aborted) {
26353
+ this.log.info('full reconnect cancelled before starting', {
26354
+ regionUrl
26355
+ });
26356
+ return;
26357
+ }
25423
26358
  if (!this.url || !this.token) {
25424
26359
  // permanent failure, don't attempt reconnection
25425
26360
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -25431,6 +26366,12 @@ class RTCEngine extends eventsExports.EventEmitter {
25431
26366
  }
25432
26367
  yield this.cleanupPeerConnections();
25433
26368
  yield this.cleanupClient();
26369
+ if (abortSignal.aborted) {
26370
+ this.log.info('full reconnect cancelled after cleaning up the previous session', {
26371
+ regionUrl
26372
+ });
26373
+ return;
26374
+ }
25434
26375
  let joinResponse;
25435
26376
  try {
25436
26377
  if (!this.signalOpts) {
@@ -25438,13 +26379,19 @@ class RTCEngine extends eventsExports.EventEmitter {
25438
26379
  throw new SignalReconnectError();
25439
26380
  }
25440
26381
  // in case a regionUrl is passed, the region URL takes precedence
25441
- joinResponse = (yield this.join(regionUrl !== null && regionUrl !== void 0 ? regionUrl : this.url, this.token, this.signalOpts, undefined, !this.options.singlePeerConnection)).joinResponse;
26382
+ joinResponse = (yield this.join(regionUrl !== null && regionUrl !== void 0 ? regionUrl : this.url, this.token, this.signalOpts, abortSignal, !this.options.singlePeerConnection)).joinResponse;
25442
26383
  } catch (e) {
25443
26384
  if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
25444
26385
  throw new UnexpectedConnectionState('could not reconnect, token might be expired');
25445
26386
  }
25446
26387
  throw new SignalReconnectError();
25447
26388
  }
26389
+ if (abortSignal.aborted) {
26390
+ this.log.info('full reconnect cancelled after the signal join response', {
26391
+ regionUrl
26392
+ });
26393
+ return;
26394
+ }
25448
26395
  if (this.shouldFailNext) {
25449
26396
  this.shouldFailNext = false;
25450
26397
  throw new Error('simulated failure');
@@ -25452,6 +26399,12 @@ class RTCEngine extends eventsExports.EventEmitter {
25452
26399
  this.client.setReconnected();
25453
26400
  this.emit(EngineEvent.SignalRestarted, joinResponse);
25454
26401
  yield this.waitForPCReconnected();
26402
+ if (abortSignal.aborted) {
26403
+ this.log.info('full reconnect cancelled after peer connections reconnected', {
26404
+ regionUrl
26405
+ });
26406
+ return;
26407
+ }
25455
26408
  // re-check signal connection state before setting engine as resumed
25456
26409
  if (this.client.currentState !== SignalConnectionState.CONNECTED) {
25457
26410
  throw new SignalReconnectError('Signal connection got severed during reconnect');
@@ -25460,9 +26413,21 @@ class RTCEngine extends eventsExports.EventEmitter {
25460
26413
  // reconnect success
25461
26414
  this.emit(EngineEvent.Restarted);
25462
26415
  } catch (error) {
25463
- const nextRegionUrl = yield (_b = this.regionStrategy) === null || _b === void 0 ? void 0 : _b.getNextUrl();
26416
+ if (abortSignal.aborted) {
26417
+ this.log.info('full reconnect cancelled after a failed attempt', {
26418
+ regionUrl
26419
+ });
26420
+ return;
26421
+ }
26422
+ const nextRegionUrl = yield (_b = this.regionStrategy) === null || _b === void 0 ? void 0 : _b.getNextUrl(abortSignal);
26423
+ if (abortSignal.aborted) {
26424
+ this.log.info('full reconnect cancelled during region selection', {
26425
+ regionUrl
26426
+ });
26427
+ return;
26428
+ }
25464
26429
  if (nextRegionUrl) {
25465
- yield this.restartConnection(nextRegionUrl);
26430
+ yield this.restartConnection(abortSignal, nextRegionUrl);
25466
26431
  return;
25467
26432
  } else {
25468
26433
  // no more regions to try (or we're not on cloud)
@@ -25657,7 +26622,11 @@ class RTCEngine extends eventsExports.EventEmitter {
25657
26622
  */
25658
26623
  waitForBufferHeadroom(kind) {
25659
26624
  return __awaiter(this, void 0, void 0, function* () {
25660
- return this.flowControlFor(kind).waitForHeadroomWithLock();
26625
+ const dc = this.dataChannelForKind(kind);
26626
+ if (!dc) {
26627
+ throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
26628
+ }
26629
+ return this.flowControlFor(kind).waitForHeadroomWithLock(dc);
25661
26630
  });
25662
26631
  }
25663
26632
  /**
@@ -25763,8 +26732,7 @@ class RTCEngine extends eventsExports.EventEmitter {
25763
26732
  if (this.isClosed) {
25764
26733
  reject(new NegotiationError('cannot negotiate on closed engine'));
25765
26734
  }
25766
- this.on(EngineEvent.Closing, handleClosed);
25767
- this.on(EngineEvent.Restarting, handleClosed);
26735
+ this.pendingNegotiationAborts.add(handleClosed);
25768
26736
  this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
25769
26737
  this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
25770
26738
  try {
@@ -25787,8 +26755,7 @@ class RTCEngine extends eventsExports.EventEmitter {
25787
26755
  reject(new Error(String(e)));
25788
26756
  }
25789
26757
  } finally {
25790
- this.off(EngineEvent.Closing, handleClosed);
25791
- this.off(EngineEvent.Restarting, handleClosed);
26758
+ this.pendingNegotiationAborts.delete(handleClosed);
25792
26759
  }
25793
26760
  }));
25794
26761
  });
@@ -26519,6 +27486,16 @@ class TextStreamReader extends BaseStreamReader {
26519
27486
  this.byteStreamHandlers.delete(topic);
26520
27487
  }
26521
27488
  clearControllers() {
27489
+ // Error any in flight streams so readers do not wait forever for chunks that can no longer arrive.
27490
+ const abnormalEndError = new DataStreamError('Room disconnected in the middle of receiving data', DataStreamErrorReason.AbnormalEnd);
27491
+ for (const _ref2 of this.byteStreamControllers.values()) {
27492
+ const controller = _ref2.controller;
27493
+ controller.error(abnormalEndError);
27494
+ }
27495
+ for (const _ref3 of this.textStreamControllers.values()) {
27496
+ const controller = _ref3.controller;
27497
+ controller.error(abnormalEndError);
27498
+ }
26522
27499
  this.byteStreamControllers.clear();
26523
27500
  this.textStreamControllers.clear();
26524
27501
  this.bufferedPackets = [];
@@ -26527,20 +27504,20 @@ class TextStreamReader extends BaseStreamReader {
26527
27504
  // Terminate any in flight data stream receives from the given participant
26528
27505
  const textStreamsBeingSentByDisconnectingParticipant = Array.from(this.textStreamControllers.entries()).filter(entry => entry[1].sendingParticipantIdentity === participantIdentity);
26529
27506
  const byteStreamsBeingSentByDisconnectingParticipant = Array.from(this.byteStreamControllers.entries()).filter(entry => entry[1].sendingParticipantIdentity === participantIdentity);
26530
- if (textStreamsBeingSentByDisconnectingParticipant.length > 0 || byteStreamsBeingSentByDisconnectingParticipant.length > 0) {
26531
- const abnormalEndError = new DataStreamError("Participant ".concat(participantIdentity, " unexpectedly disconnected in the middle of sending data"), DataStreamErrorReason.AbnormalEnd);
26532
- for (const _ref2 of byteStreamsBeingSentByDisconnectingParticipant) {
26533
- var _ref3 = _slicedToArray(_ref2, 2);
26534
- const id = _ref3[0];
26535
- const controller = _ref3[1];
26536
- controller.controller.error(abnormalEndError);
26537
- this.byteStreamControllers.delete(id);
26538
- }
26539
- for (const _ref4 of textStreamsBeingSentByDisconnectingParticipant) {
27507
+ if (textStreamsBeingSentByDisconnectingParticipant.length > 0 || byteStreamsBeingSentByDisconnectingParticipant.length > 0) {
27508
+ const abnormalEndError = new DataStreamError("Participant ".concat(participantIdentity, " unexpectedly disconnected in the middle of sending data"), DataStreamErrorReason.AbnormalEnd);
27509
+ for (const _ref4 of byteStreamsBeingSentByDisconnectingParticipant) {
26540
27510
  var _ref5 = _slicedToArray(_ref4, 2);
26541
27511
  const id = _ref5[0];
26542
27512
  const controller = _ref5[1];
26543
27513
  controller.controller.error(abnormalEndError);
27514
+ this.byteStreamControllers.delete(id);
27515
+ }
27516
+ for (const _ref6 of textStreamsBeingSentByDisconnectingParticipant) {
27517
+ var _ref7 = _slicedToArray(_ref6, 2);
27518
+ const id = _ref7[0];
27519
+ const controller = _ref7[1];
27520
+ controller.controller.error(abnormalEndError);
26544
27521
  this.textStreamControllers.delete(id);
26545
27522
  }
26546
27523
  }
@@ -26589,10 +27566,10 @@ class TextStreamReader extends BaseStreamReader {
26589
27566
  let compressed;
26590
27567
  switch (streamHeader.compression) {
26591
27568
  case DataStream_CompressionType.DEFLATE_RAW:
26592
- if (!isCompressionStreamSupported()) {
27569
+ if (!isDeflateRawCompressionSupported()) {
26593
27570
  // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
26594
27571
  // isn't properly abiding by the data streams v2 protocol.
26595
- livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
27572
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not support deflate-raw decompression. Dropping..."));
26596
27573
  return;
26597
27574
  }
26598
27575
  compressed = true;
@@ -26660,10 +27637,10 @@ class TextStreamReader extends BaseStreamReader {
26660
27637
  let compressed;
26661
27638
  switch (streamHeader.compression) {
26662
27639
  case DataStream_CompressionType.DEFLATE_RAW:
26663
- if (!isCompressionStreamSupported()) {
27640
+ if (!isDeflateRawCompressionSupported()) {
26664
27641
  // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
26665
27642
  // isn't properly abiding by the data streams v2 protocol.
26666
- livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
27643
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not support deflate-raw decompression. Dropping..."));
26667
27644
  return;
26668
27645
  }
26669
27646
  compressed = true;
@@ -27028,7 +28005,7 @@ class OutgoingDataStreamManager {
27028
28005
  attributes: options === null || options === void 0 ? void 0 : options.attributes,
27029
28006
  encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
27030
28007
  };
27031
- const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities) && this.allRecipientsSupportCompression(options === null || options === void 0 ? void 0 : options.destinationIdentities);
28008
+ const compressEligible = compress && isDeflateRawCompressionSupported() && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities) && this.allRecipientsSupportCompression(options === null || options === void 0 ? void 0 : options.destinationIdentities);
27032
28009
  let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(textInBytes).pipeThrough(deflateRawTransform())) : null;
27033
28010
  // Phase 1: Try to send as a single packet data stream
27034
28011
  const noAttachments = !(options === null || options === void 0 ? void 0 : options.attachments) || options.attachments.length === 0;
@@ -27137,7 +28114,7 @@ class OutgoingDataStreamManager {
27137
28114
  encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
27138
28115
  };
27139
28116
  const progressMonitorTap = progressReportingStream(bytes.length, options === null || options === void 0 ? void 0 : options.onProgress);
27140
- const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
28117
+ const compressEligible = compress && isDeflateRawCompressionSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
27141
28118
  let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(bytes).pipeThrough(progressMonitorTap).pipeThrough(deflateRawTransform())) : null;
27142
28119
  // Phase 1: Try to send as a single packet data stream
27143
28120
  if (this.allRecipientsSupportV2(destinationIdentities)) {
@@ -27334,7 +28311,7 @@ class OutgoingDataStreamManager {
27334
28311
  return __awaiter(this, void 0, void 0, function* () {
27335
28312
  var _a, _b, _c, _d, _e;
27336
28313
  const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
27337
- const compress = ((_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true) && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
28314
+ const compress = ((_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true) && isDeflateRawCompressionSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
27338
28315
  const info = {
27339
28316
  id: streamId,
27340
28317
  name: file.name,
@@ -27537,58 +28514,6 @@ function sendStreamTrailer(streamId, destinationIdentities, engine) {
27537
28514
  });
27538
28515
  yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
27539
28516
  });
27540
- }/**
27541
- * Implementation of AbortSignal.any
27542
- * Creates a signal that will be aborted when any of the given signals is aborted.
27543
- * @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any
27544
- */
27545
- function abortSignalAny(signals) {
27546
- // Handle empty signals array
27547
- if (signals.length === 0) {
27548
- const controller = new AbortController();
27549
- return controller.signal;
27550
- }
27551
- // Fast path for single signal
27552
- if (signals.length === 1) {
27553
- return signals[0];
27554
- }
27555
- // Check if any signal is already aborted
27556
- for (const signal of signals) {
27557
- if (signal.aborted) {
27558
- return signal;
27559
- }
27560
- }
27561
- // Create a new controller for the combined signal
27562
- const controller = new AbortController();
27563
- const unlisteners = Array(signals.length);
27564
- // Function to clean up all event listeners
27565
- const cleanup = () => {
27566
- for (const unsubscribe of unlisteners) {
27567
- unsubscribe();
27568
- }
27569
- };
27570
- // Add event listeners to each signal
27571
- signals.forEach((signal, index) => {
27572
- const handler = () => {
27573
- controller.abort(signal.reason);
27574
- cleanup();
27575
- };
27576
- signal.addEventListener('abort', handler);
27577
- unlisteners[index] = () => signal.removeEventListener('abort', handler);
27578
- });
27579
- return controller.signal;
27580
- }
27581
- /**
27582
- * Implementation of AbortSignal.timeout
27583
- * Creates a signal that will be aborted after the specified timeout.
27584
- * @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
27585
- */
27586
- function abortSignalTimeout(ms) {
27587
- const controller = new AbortController();
27588
- setTimeout(() => {
27589
- controller.abort(new DOMException("signal timed out after ".concat(ms, " ms"), 'TimeoutError'));
27590
- }, ms);
27591
- return controller.signal;
27592
28517
  }// Number type sizes
27593
28518
  const U8_LENGTH_BYTES = 1;
27594
28519
  const U16_LENGTH_BYTES = 2;
@@ -29945,6 +30870,12 @@ function truncateBytes(str, maxBytes) {
29945
30870
  high = mid - 1;
29946
30871
  }
29947
30872
  }
30873
+ // Never end on the first half of a surrogate pair: it would be encoded as a replacement
30874
+ // character, and the truncated string would no longer round trip.
30875
+ const lastCode = low > 0 ? str.charCodeAt(low - 1) : 0;
30876
+ if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
30877
+ low -= 1;
30878
+ }
29948
30879
  return str.slice(0, low);
29949
30880
  }/**
29950
30881
  * Manages the client (caller) side of RPC: sending requests, tracking pending
@@ -30005,20 +30936,37 @@ class RpcClientManager extends eventsExports.EventEmitter {
30005
30936
  completionFuture,
30006
30937
  participantIdentity: destinationIdentity
30007
30938
  });
30008
- yield _this.publishRpcRequest(destinationIdentity, id, method, payload, effectiveTimeoutMs, remoteClientProtocol);
30009
- responseTimeoutId = setTimeout(() => {
30010
- var _a;
30011
- _this.pendingResponses.delete(id);
30012
- (_a = completionFuture.reject) === null || _a === void 0 ? void 0 : _a.call(completionFuture, RpcError.builtIn('RESPONSE_TIMEOUT'));
30013
- }, responseTimeoutMs);
30939
+ // Observe the future before publishing: a slow send can outlast the ack timer.
30940
+ // Keep the returned promise rejected for callers, while marking its rejection
30941
+ // handled until performRpc has finished publishing and can return it.
30014
30942
  const completionPromise = completionFuture.promise.finally(() => {
30015
- clearTimeout(responseTimeoutId);
30943
+ if (responseTimeoutId !== null) {
30944
+ clearTimeout(responseTimeoutId);
30945
+ }
30016
30946
  if (_this.pendingAcks.has(id)) {
30017
30947
  _this.log.warn('RPC response received before ack', id);
30018
30948
  _this.pendingAcks.delete(id);
30019
30949
  clearTimeout(ackTimeoutId);
30020
30950
  }
30021
30951
  });
30952
+ completionPromise.catch(() => {});
30953
+ try {
30954
+ yield _this.publishRpcRequest(destinationIdentity, id, method, payload, effectiveTimeoutMs, remoteClientProtocol);
30955
+ } catch (e) {
30956
+ // The caller receives the publish error. Do not leave an ack timer that
30957
+ // later rejects a future the caller never received.
30958
+ clearTimeout(ackTimeoutId);
30959
+ _this.pendingAcks.delete(id);
30960
+ _this.pendingResponses.delete(id);
30961
+ throw e;
30962
+ }
30963
+ if (_this.pendingResponses.has(id)) {
30964
+ responseTimeoutId = setTimeout(() => {
30965
+ var _a;
30966
+ _this.pendingResponses.delete(id);
30967
+ (_a = completionFuture.reject) === null || _a === void 0 ? void 0 : _a.call(completionFuture, RpcError.builtIn('RESPONSE_TIMEOUT'));
30968
+ }, responseTimeoutMs);
30969
+ }
30022
30970
  return [id, completionPromise];
30023
30971
  }();
30024
30972
  });
@@ -30332,6 +31280,12 @@ class RpcServerManager extends eventsExports.EventEmitter {
30332
31280
  }class RemoteAudioTrack extends RemoteTrack {
30333
31281
  constructor(mediaTrack, sid, receiver, audioContext, audioOutput, loggerOptions) {
30334
31282
  super(mediaTrack, sid, Track.Kind.Audio, receiver, loggerOptions);
31283
+ /**
31284
+ * the nodes of the graph currently wired up, in order, so teardown can undo exactly the edges
31285
+ * `connectWebAudio` created. Reading `webAudioPluginNodes` instead would miss the old plugins
31286
+ * once `setWebAudioPlugins` has replaced them.
31287
+ */
31288
+ this.connectedNodes = [];
30335
31289
  this.monitorReceiver = () => __awaiter(this, void 0, void 0, function* () {
30336
31290
  if (!this.receiver) {
30337
31291
  this._currentBitrate = 0;
@@ -30354,10 +31308,11 @@ class RpcServerManager extends eventsExports.EventEmitter {
30354
31308
  */
30355
31309
  setVolume(volume) {
30356
31310
  var _a;
30357
- for (const el of this.attachedElements) {
30358
- if (this.audioContext) {
30359
- (_a = this.gainNode) === null || _a === void 0 ? void 0 : _a.gain.setTargetAtTime(volume, 0, 0.1);
30360
- } else {
31311
+ if (this.audioContext) {
31312
+ // playback goes through the web audio graph, the attached elements themselves stay silent
31313
+ (_a = this.gainNode) === null || _a === void 0 ? void 0 : _a.gain.setTargetAtTime(volume, 0, 0.1);
31314
+ } else {
31315
+ for (const el of this.attachedElements) {
30361
31316
  el.volume = volume;
30362
31317
  }
30363
31318
  }
@@ -30371,7 +31326,7 @@ class RpcServerManager extends eventsExports.EventEmitter {
30371
31326
  * gets the volume of attached audio elements (loudest)
30372
31327
  */
30373
31328
  getVolume() {
30374
- if (this.elementVolume) {
31329
+ if (this.elementVolume !== undefined) {
30375
31330
  return this.elementVolume;
30376
31331
  }
30377
31332
  if (isReactNative()) {
@@ -30403,7 +31358,9 @@ class RpcServerManager extends eventsExports.EventEmitter {
30403
31358
  });
30404
31359
  }
30405
31360
  attach(element) {
30406
- const needsNewWebAudioConnection = this.attachedElements.length === 0;
31361
+ // the graph is fed by the track rather than by a specific element, so one connection covers
31362
+ // all attached elements
31363
+ const needsNewWebAudioConnection = !this.sourceNode;
30407
31364
  if (!element) {
30408
31365
  element = super.attach();
30409
31366
  } else {
@@ -30414,13 +31371,17 @@ class RpcServerManager extends eventsExports.EventEmitter {
30414
31371
  this.log.error('Failed to set sink id on remote audio track', e, this.logContext);
30415
31372
  });
30416
31373
  }
30417
- if (this.audioContext && needsNewWebAudioConnection) {
30418
- this.log.debug('using audio context mapping', this.logContext);
30419
- this.connectWebAudio(this.audioContext, element);
31374
+ if (this.audioContext) {
31375
+ // audio is routed through the web audio graph, so every attached element has to stay
31376
+ // silent, no matter whether it is the one the graph was built from
30420
31377
  element.volume = 0;
30421
31378
  element.muted = true;
31379
+ if (needsNewWebAudioConnection) {
31380
+ this.log.debug('using audio context mapping', this.logContext);
31381
+ this.connectWebAudio(this.audioContext, element);
31382
+ }
30422
31383
  }
30423
- if (this.elementVolume) {
31384
+ if (this.elementVolume !== undefined) {
30424
31385
  // make sure volume setting is being applied to the newly attached element
30425
31386
  this.setVolume(this.elementVolume);
30426
31387
  }
@@ -30450,11 +31411,27 @@ class RpcServerManager extends eventsExports.EventEmitter {
30450
31411
  * @experimental
30451
31412
  */
30452
31413
  setAudioContext(audioContext) {
31414
+ var _a;
31415
+ if (this.audioContext === audioContext) {
31416
+ return;
31417
+ }
30453
31418
  this.audioContext = audioContext;
30454
- if (audioContext && this.attachedElements.length > 0) {
30455
- this.connectWebAudio(audioContext, this.attachedElements[0]);
30456
- } else if (!audioContext) {
31419
+ if (audioContext) {
31420
+ // playback moves into the web audio graph, mute the elements to avoid double playback
31421
+ for (const element of this.attachedElements) {
31422
+ element.volume = 0;
31423
+ element.muted = true;
31424
+ }
31425
+ if (this.attachedElements.length > 0) {
31426
+ this.connectWebAudio(audioContext, this.attachedElements[0]);
31427
+ }
31428
+ } else {
30457
31429
  this.disconnectWebAudio();
31430
+ // playback moves back into the elements, which `attach` had muted in favour of the graph
31431
+ for (const element of this.attachedElements) {
31432
+ element.muted = false;
31433
+ element.volume = (_a = this.elementVolume) !== null && _a !== void 0 ? _a : 1;
31434
+ }
30458
31435
  }
30459
31436
  }
30460
31437
  /**
@@ -30473,14 +31450,17 @@ class RpcServerManager extends eventsExports.EventEmitter {
30473
31450
  // @ts-ignore attached elements always have a srcObject set
30474
31451
  this.sourceNode = context.createMediaStreamSource(element.srcObject);
30475
31452
  let lastNode = this.sourceNode;
31453
+ this.connectedNodes = [this.sourceNode];
30476
31454
  this.webAudioPluginNodes.forEach(node => {
30477
31455
  lastNode.connect(node);
30478
31456
  lastNode = node;
31457
+ this.connectedNodes.push(node);
30479
31458
  });
30480
31459
  this.gainNode = context.createGain();
30481
31460
  lastNode.connect(this.gainNode);
31461
+ this.connectedNodes.push(this.gainNode);
30482
31462
  this.gainNode.connect(context.destination);
30483
- if (this.elementVolume) {
31463
+ if (this.elementVolume !== undefined) {
30484
31464
  this.gainNode.gain.setTargetAtTime(this.elementVolume, 0, 0.1);
30485
31465
  }
30486
31466
  // try to resume the context if it isn't running already
@@ -30496,6 +31476,19 @@ class RpcServerManager extends eventsExports.EventEmitter {
30496
31476
  }
30497
31477
  disconnectWebAudio() {
30498
31478
  var _a, _b;
31479
+ // the plugin nodes belong to whoever passed them in and may feed graphs of their own, so only
31480
+ // the edges this track created come down. A bare `node.disconnect()` would drop the
31481
+ // application's own routing with them
31482
+ for (let i = 0; i < this.connectedNodes.length - 1; i += 1) {
31483
+ try {
31484
+ this.connectedNodes[i].disconnect(this.connectedNodes[i + 1]);
31485
+ } catch (_c) {
31486
+ // a plugin node the application already disconnected itself, the rest of the chain still
31487
+ // has to come apart
31488
+ }
31489
+ }
31490
+ this.connectedNodes = [];
31491
+ // the source and gain nodes are ours alone, so every remaining edge of theirs is ours to drop
30499
31492
  (_a = this.gainNode) === null || _a === void 0 ? void 0 : _a.disconnect();
30500
31493
  (_b = this.sourceNode) === null || _b === void 0 ? void 0 : _b.disconnect();
30501
31494
  this.gainNode = undefined;
@@ -31157,8 +32150,10 @@ class Participant extends eventsExports.EventEmitter {
31157
32150
  * @internal
31158
32151
  */
31159
32152
  setAudioContext(ctx) {
31160
- this.audioContext = ctx;
31161
- this.audioTrackPublications.forEach(track => isAudioTrack(track.track) && track.track.setAudioContext(ctx));
32153
+ return __awaiter(this, void 0, void 0, function* () {
32154
+ this.audioContext = ctx;
32155
+ yield Promise.all(Array.from(this.audioTrackPublications.values()).map(publication => publication.track).filter(isAudioTrack).map(track => track.setAudioContext(ctx)));
32156
+ });
31162
32157
  }
31163
32158
  addTrackPublication(publication) {
31164
32159
  this.log.debug("adding track publication", {
@@ -31417,10 +32412,7 @@ class Participant extends eventsExports.EventEmitter {
31417
32412
  if (!track.isMuted) {
31418
32413
  this.log.debug('track ended, attempting to use a different device', getLogContextFromTrack(track));
31419
32414
  if (isLocalAudioTrack(track)) {
31420
- // fall back to default device if available
31421
- yield track.restartTrack({
31422
- deviceId: 'default'
31423
- });
32415
+ yield this.restartOnDefaultAudioDevice(track);
31424
32416
  } else {
31425
32417
  yield track.restartTrack();
31426
32418
  }
@@ -31767,10 +32759,10 @@ class Participant extends eventsExports.EventEmitter {
31767
32759
  loggerName: this.roomOptions.loggerName,
31768
32760
  loggerContextCb: () => this.logContext
31769
32761
  });
31770
- const localTracks = tracks.map(track => {
32762
+ const localTracks = yield Promise.all(tracks.map(track => __awaiter(this, void 0, void 0, function* () {
31771
32763
  if (isAudioTrack(track)) {
31772
32764
  this.microphoneError = undefined;
31773
- track.setAudioContext(this.audioContext);
32765
+ yield track.setAudioContext(this.audioContext);
31774
32766
  track.source = Track.Source.Microphone;
31775
32767
  this.emit(ParticipantEvent.AudioStreamAcquired);
31776
32768
  }
@@ -31779,7 +32771,7 @@ class Participant extends eventsExports.EventEmitter {
31779
32771
  track.source = Track.Source.Camera;
31780
32772
  }
31781
32773
  return track;
31782
- });
32774
+ })));
31783
32775
  return localTracks;
31784
32776
  } catch (err) {
31785
32777
  if (err instanceof Error) {
@@ -31889,7 +32881,7 @@ class Participant extends eventsExports.EventEmitter {
31889
32881
  return function* () {
31890
32882
  var _a, _b, _c, _d;
31891
32883
  if (isLocalAudioTrack(track)) {
31892
- track.setAudioContext(_this2.audioContext);
32884
+ yield track.setAudioContext(_this2.audioContext);
31893
32885
  }
31894
32886
  yield (_a = _this2.reconnectFuture) === null || _a === void 0 ? void 0 : _a.promise;
31895
32887
  if (_this2.republishPromise && !isRepublish) {
@@ -32489,19 +33481,36 @@ class Participant extends eventsExports.EventEmitter {
32489
33481
  return;
32490
33482
  }
32491
33483
  const packetTrailerFeatures = this.normalizeRequestedFrameMetadataOptions(track, opts);
33484
+ // the backup sender encodes the processed track, not the raw capture track.dimensions reports
33485
+ const _track$mediaStreamTra = track.mediaStreamTrack.getSettings(),
33486
+ width = _track$mediaStreamTra.width,
33487
+ height = _track$mediaStreamTra.height;
33488
+ const dims = width && height ? {
33489
+ width,
33490
+ height
33491
+ } : undefined;
33492
+ if (!dims) {
33493
+ this.log.warn('could not determine track dimensions, publishing backup codec without layers', getLogContextFromTrack(track));
33494
+ }
33495
+ // the server reads the layers off the codec entry for a request that carries a sid, and
33496
+ // falls back to the top level ones when it predates per codec layers
33497
+ const layers = dims ? videoLayersFromEncodings(dims.width, dims.height, encodings) : [];
32492
33498
  const req = new AddTrackRequest({
32493
33499
  cid: simulcastTrack.mediaStreamTrack.id,
32494
33500
  type: Track.kindToProto(track.kind),
32495
33501
  muted: track.isMuted,
32496
33502
  source: Track.sourceToProto(track.source),
32497
33503
  sid: track.sid,
33504
+ width: dims === null || dims === void 0 ? void 0 : dims.width,
33505
+ height: dims === null || dims === void 0 ? void 0 : dims.height,
32498
33506
  packetTrailerFeatures,
33507
+ layers,
32499
33508
  simulcastCodecs: [{
32500
33509
  codec: opts.videoCodec,
32501
- cid: simulcastTrack.mediaStreamTrack.id
33510
+ cid: simulcastTrack.mediaStreamTrack.id,
33511
+ layers
32502
33512
  }]
32503
33513
  });
32504
- req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
32505
33514
  if (!this.engine || this.engine.isClosed) {
32506
33515
  throw new UnexpectedConnectionState('cannot publish track when not connected');
32507
33516
  }
@@ -32921,6 +33930,35 @@ class Participant extends eventsExports.EventEmitter {
32921
33930
  }
32922
33931
  return this.activeAgentFuture.promise;
32923
33932
  }
33933
+ /**
33934
+ * @internal
33935
+ */
33936
+ restartOnDefaultAudioDevice(track) {
33937
+ return __awaiter(this, void 0, void 0, function* () {
33938
+ var _a, _b;
33939
+ try {
33940
+ if (isChromiumBased()) {
33941
+ // only chrome has the notion of a literal `default` device
33942
+ yield track.setDeviceId({
33943
+ exact: 'default'
33944
+ });
33945
+ } else {
33946
+ // just try to restart for other browsers and try to change the device id as a fallback in the catch clause
33947
+ yield track.restartTrack();
33948
+ }
33949
+ } catch (e) {
33950
+ // restart() clears the manually-stopped flag on entry, so retrying a track the user stopped
33951
+ // while the first attempt was in flight would re-open the capture device behind their back
33952
+ if (!isDeviceAcquisitionFailure(e) || isPermissionDeniedError(e) || track.isStopped) {
33953
+ throw e;
33954
+ }
33955
+ this.log.debug('could not reacquire the audio device, retrying with an enumerated device', getLogContextFromTrack(track));
33956
+ // permission is already granted at this point, so skip the probe that would re-acquire media
33957
+ const allDevices = yield DeviceManager.getInstance().getDevices('audioinput', false);
33958
+ yield track.setDeviceId((_b = (_a = allDevices === null || allDevices === void 0 ? void 0 : allDevices[0]) === null || _a === void 0 ? void 0 : _a.deviceId) !== null && _b !== void 0 ? _b : 'default');
33959
+ }
33960
+ });
33961
+ }
32924
33962
  getPublicationForTrack(track) {
32925
33963
  let publication;
32926
33964
  this.trackPublications.forEach(pub => {
@@ -33631,6 +34669,30 @@ class DeferrableMap extends Map {
33631
34669
  })(ConnectionState || (ConnectionState = {}));
33632
34670
  const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
33633
34671
  const STATS_LOG_FREQUENCY_MS = 30 * 1000;
34672
+ const DUMMY_AUDIO_ELEMENT_ID = 'livekit-dummy-audio-el';
34673
+ /**
34674
+ * One silent audio element serves every room on the page, so it is reference counted: a room
34675
+ * disconnecting must not pull it out from under the rooms still playing through it. The element
34676
+ * and its track go away once the last room hands its reference back.
34677
+ */
34678
+ let sharedDummyAudioElement;
34679
+ function createSharedDummyAudioElement() {
34680
+ const element = document.createElement('audio');
34681
+ element.id = DUMMY_AUDIO_ELEMENT_ID;
34682
+ element.autoplay = true;
34683
+ element.hidden = true;
34684
+ const track = getEmptyAudioStreamTrack();
34685
+ track.enabled = true;
34686
+ const stream = new MediaStream([track]);
34687
+ element.srcObject = stream;
34688
+ document.body.append(element);
34689
+ return {
34690
+ element,
34691
+ stream,
34692
+ track,
34693
+ refCount: 0
34694
+ };
34695
+ }
33634
34696
  /**
33635
34697
  * In LiveKit, a room is the logical grouping for a list of participants.
33636
34698
  * Participants in a room can publish tracks, and subscribe to others' tracks.
@@ -33661,6 +34723,24 @@ class Room extends eventsExports.EventEmitter {
33661
34723
  /** reflects the sender encryption status of the local participant */
33662
34724
  this.isE2EEEnabled = false;
33663
34725
  this.audioEnabled = true;
34726
+ /**
34727
+ * whether `audioContext` was created by the SDK and therefore has to be closed by it.
34728
+ * A context handed in through `webAudioMix` is owned by the application instead.
34729
+ */
34730
+ this.ownsAudioContext = false;
34731
+ /**
34732
+ * set once the audio context has been released as part of a disconnect. Guards against
34733
+ * creating a new context outside of a connection lifecycle, where nothing would close it again.
34734
+ */
34735
+ this.audioContextReleased = false;
34736
+ /**
34737
+ * serialises `acquireAudioContext` against `releaseAudioContext`. Both await per-track graph
34738
+ * rebuilds, so without it a reconnect landing inside a disconnect's teardown leaves the two
34739
+ * racing to set the participants' context, and the loser's value is the one that sticks.
34740
+ */
34741
+ this.audioContextMutex = new _();
34742
+ /** silent audio tracks this room acquired, handed back to the shared source on disconnect */
34743
+ this.acquiredEmptyAudioTracks = new Set();
33664
34744
  this.e2eeStateMutex = new _();
33665
34745
  this.isVideoPlaybackBlocked = false;
33666
34746
  this.log = livekitLogger;
@@ -33731,7 +34811,7 @@ class Room extends eventsExports.EventEmitter {
33731
34811
  this.abortController = undefined;
33732
34812
  resolve();
33733
34813
  } catch (error) {
33734
- if (this.regionUrlProvider && error instanceof ConnectionError && error.reason !== ConnectionErrorReason.Cancelled && error.reason !== ConnectionErrorReason.NotAllowed) {
34814
+ if (this.regionUrlProvider && error instanceof ConnectionError && canFailOverToAnotherRegion(error)) {
33735
34815
  let nextUrl = null;
33736
34816
  try {
33737
34817
  this.log.debug('Fetching next region');
@@ -33780,6 +34860,8 @@ class Room extends eventsExports.EventEmitter {
33780
34860
  const _yield$engine$join = yield engine.join(url, token, {
33781
34861
  autoSubscribe: connectOptions.autoSubscribe,
33782
34862
  adaptiveStream: typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
34863
+ disableIceLite: connectOptions.disableIceLite,
34864
+ subscriberAllowPause: connectOptions.subscriberAllowPause,
33783
34865
  clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
33784
34866
  maxRetries: connectOptions.maxRetries,
33785
34867
  e2eeEnabled: !!this.e2eeManager,
@@ -33831,7 +34913,9 @@ class Room extends eventsExports.EventEmitter {
33831
34913
  if ((_b = this.regionUrlProvider) === null || _b === void 0 ? void 0 : _b.isCloud()) {
33832
34914
  this.engine.setRegionStrategy(this.createRegionStrategy());
33833
34915
  }
33834
- this.acquireAudioContext();
34916
+ this.acquireAudioContext(true).catch(error => this.log.warn('Could not acquire audio context', Object.assign(Object.assign({}, this.logContext), {
34917
+ error
34918
+ })));
33835
34919
  this.connOptions = Object.assign(Object.assign({}, roomConnectOptionDefaults), opts);
33836
34920
  if (this.connOptions.rtcConfig) {
33837
34921
  this.engine.rtcConfig = this.connOptions.rtcConfig;
@@ -33950,44 +35034,8 @@ class Room extends eventsExports.EventEmitter {
33950
35034
  this.startAudio = () => __awaiter(this, void 0, void 0, function* () {
33951
35035
  const elements = [];
33952
35036
  const browser = getBrowser();
33953
- if (browser && browser.os === 'iOS') {
33954
- /**
33955
- * iOS blocks audio element playback if
33956
- * - user is not publishing audio themselves and
33957
- * - no other audio source is playing
33958
- *
33959
- * as a workaround, we create an audio element with an empty track, so that
33960
- * silent audio is always playing
33961
- */
33962
- const audioId = 'livekit-dummy-audio-el';
33963
- let dummyAudioEl = document.getElementById(audioId);
33964
- if (!dummyAudioEl) {
33965
- dummyAudioEl = document.createElement('audio');
33966
- dummyAudioEl.id = audioId;
33967
- dummyAudioEl.autoplay = true;
33968
- dummyAudioEl.hidden = true;
33969
- const track = getEmptyAudioStreamTrack();
33970
- track.enabled = true;
33971
- const stream = new MediaStream([track]);
33972
- dummyAudioEl.srcObject = stream;
33973
- document.addEventListener('visibilitychange', () => {
33974
- if (!dummyAudioEl) {
33975
- return;
33976
- }
33977
- // set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
33978
- dummyAudioEl.srcObject = document.hidden ? null : stream;
33979
- if (!document.hidden) {
33980
- this.log.debug('page visible again, triggering startAudio to resume playback and update playback status');
33981
- this.startAudio();
33982
- }
33983
- });
33984
- document.body.append(dummyAudioEl);
33985
- this.once(RoomEvent.Disconnected, () => {
33986
- dummyAudioEl === null || dummyAudioEl === void 0 ? void 0 : dummyAudioEl.remove();
33987
- dummyAudioEl = null;
33988
- });
33989
- }
33990
- elements.push(dummyAudioEl);
35037
+ if (browser && browser.os === 'iOS' && !this.audioContextReleased) {
35038
+ elements.push(this.acquireDummyAudioElement());
33991
35039
  }
33992
35040
  this.remoteParticipants.forEach(p => {
33993
35041
  p.audioTrackPublications.forEach(t => {
@@ -33999,7 +35047,10 @@ class Room extends eventsExports.EventEmitter {
33999
35047
  });
34000
35048
  });
34001
35049
  try {
34002
- yield Promise.all([this.acquireAudioContext(), ...elements.map(e => {
35050
+ yield Promise.all([
35051
+ // once the audio context has been released as part of a disconnect there is nothing left
35052
+ // to unlock, and creating a new context here would leak it as no disconnect will follow
35053
+ ...(this.audioContextReleased ? [] : [this.acquireAudioContext()]), ...elements.map(e => {
34003
35054
  // when webAudioMix is enabled, attached elements are deliberately kept muted by
34004
35055
  // RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
34005
35056
  // unmuting them here would cause double playback on platforms where element.volume
@@ -34218,7 +35269,7 @@ class Room extends eventsExports.EventEmitter {
34218
35269
  // find the participant
34219
35270
  const participant = this.remoteParticipants.get(packet.participantIdentity);
34220
35271
  if (packet.value.case === 'user') {
34221
- this.handleUserPacket(participant, packet.value.value, packet.kind, encryptionType);
35272
+ this.handleUserPacket(participant, packet.value.value, packet.kind, encryptionType, packet.participantIdentity);
34222
35273
  } else if (packet.value.case === 'transcription') {
34223
35274
  this.handleTranscription(participant, packet.value.value);
34224
35275
  } else if (packet.value.case === 'sipDtmf') {
@@ -34249,8 +35300,8 @@ class Room extends eventsExports.EventEmitter {
34249
35300
  this.rpcClientManager.handleIncomingRpcAck(packet.value.value.requestId);
34250
35301
  }
34251
35302
  };
34252
- this.handleUserPacket = (participant, userPacket, kind, encryptionType) => {
34253
- this.emit(RoomEvent.DataReceived, userPacket.payload, participant, kind, userPacket.topic, encryptionType);
35303
+ this.handleUserPacket = (participant, userPacket, kind, encryptionType, participantIdentity) => {
35304
+ this.emit(RoomEvent.DataReceived, userPacket.payload, participant, kind, userPacket.topic, encryptionType, participantIdentity);
34254
35305
  // also emit on the participant
34255
35306
  participant === null || participant === void 0 ? void 0 : participant.emit(ParticipantEvent.DataReceived, userPacket.payload, kind, encryptionType);
34256
35307
  };
@@ -34825,6 +35876,12 @@ class Room extends eventsExports.EventEmitter {
34825
35876
  updatedAtInMs: Date.now(),
34826
35877
  maxAgeInMs: DEFAULT_MAX_AGE_MS
34827
35878
  });
35879
+ }).on(EngineEvent.RequestSubscribedCodecRefresh, () => {
35880
+ for (const videoPub of this.localParticipant.videoTrackPublications.values()) {
35881
+ if (isLocalVideoTrack(videoPub.track)) {
35882
+ videoPub.track.refreshSubscribedCodecs();
35883
+ }
35884
+ }
34828
35885
  });
34829
35886
  if (this.localParticipant) {
34830
35887
  this.localParticipant.setupEngine(this.engine);
@@ -34921,8 +35978,9 @@ class Room extends eventsExports.EventEmitter {
34921
35978
  let req;
34922
35979
  switch (scenario) {
34923
35980
  case 'signal-reconnect':
35981
+ const reconnectDelay = typeof arg === 'number' ? arg : 0;
34924
35982
  // @ts-expect-error function is private
34925
- yield this.engine.client.handleOnClose('simulate disconnect');
35983
+ yield this.engine.client.handleOnClose('simulate disconnect', undefined, reconnectDelay);
34926
35984
  break;
34927
35985
  case 'fail-on-v1-path':
34928
35986
  this.engine.failNextV1Path();
@@ -35036,6 +36094,78 @@ class Room extends eventsExports.EventEmitter {
35036
36094
  }
35037
36095
  });
35038
36096
  }
36097
+ /**
36098
+ * iOS blocks audio element playback if
36099
+ * - user is not publishing audio themselves and
36100
+ * - no other audio source is playing
36101
+ *
36102
+ * as a workaround, we create an audio element with an empty track, so that
36103
+ * silent audio is always playing
36104
+ *
36105
+ * The element is shared with any other room on the page and handed back on disconnect, so it
36106
+ * outlives whichever room happened to create it.
36107
+ */
36108
+ acquireDummyAudioElement() {
36109
+ if (!sharedDummyAudioElement) {
36110
+ sharedDummyAudioElement = createSharedDummyAudioElement();
36111
+ }
36112
+ const shared = sharedDummyAudioElement;
36113
+ if (this.releaseDummyAudioElement) {
36114
+ // this room already holds a reference, a second one would never be handed back
36115
+ return shared.element;
36116
+ }
36117
+ shared.refCount += 1;
36118
+ // the listener is per room rather than shared, so that the room it resumes playback for is
36119
+ // still connected, and so that it goes away with the room that installed it
36120
+ const handleVisibilityChange = () => {
36121
+ // set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
36122
+ shared.element.srcObject = document.hidden ? null : shared.stream;
36123
+ if (!document.hidden) {
36124
+ this.log.debug('page visible again, triggering startAudio to resume playback and update playback status');
36125
+ this.startAudio().catch(error => this.log.warn('Could not restart audio on page becoming visible', Object.assign(Object.assign({}, this.logContext), {
36126
+ error
36127
+ })));
36128
+ }
36129
+ };
36130
+ document.addEventListener('visibilitychange', handleVisibilityChange);
36131
+ this.releaseDummyAudioElement = () => {
36132
+ this.releaseDummyAudioElement = undefined;
36133
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
36134
+ shared.refCount -= 1;
36135
+ if (shared.refCount > 0) {
36136
+ return;
36137
+ }
36138
+ shared.element.srcObject = null;
36139
+ shared.element.remove();
36140
+ sharedDummyAudioElement = undefined;
36141
+ releaseEmptyAudioStreamTrack(shared.track).catch(error => this.log.warn('Could not release the dummy audio element track', Object.assign(Object.assign({}, this.logContext), {
36142
+ error
36143
+ })));
36144
+ };
36145
+ return shared.element;
36146
+ }
36147
+ /**
36148
+ * Obtains a silent audio track from the shared source and keeps track of it, so that it can be
36149
+ * handed back on disconnect and the shared `AudioContext` behind it can be closed again.
36150
+ */
36151
+ acquireEmptyAudioStreamTrack() {
36152
+ const track = getEmptyAudioStreamTrack();
36153
+ this.acquiredEmptyAudioTracks.add(track);
36154
+ return track;
36155
+ }
36156
+ releaseAcquiredEmptyAudioTrack(track) {
36157
+ if (!this.acquiredEmptyAudioTracks.delete(track)) {
36158
+ return;
36159
+ }
36160
+ releaseEmptyAudioStreamTrack(track).catch(error => this.log.warn('Could not release empty audio stream track', Object.assign(Object.assign({}, this.logContext), {
36161
+ error
36162
+ })));
36163
+ }
36164
+ releaseAcquiredEmptyAudioTracks() {
36165
+ for (const track of this.acquiredEmptyAudioTracks) {
36166
+ this.releaseAcquiredEmptyAudioTrack(track);
36167
+ }
36168
+ }
35039
36169
  /**
35040
36170
  * Returns true if audio playback is enabled
35041
36171
  */
@@ -35318,7 +36448,7 @@ class Room extends eventsExports.EventEmitter {
35318
36448
  handleDisconnect() {
35319
36449
  let shouldStopTracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
35320
36450
  let reason = arguments.length > 1 ? arguments[1] : undefined;
35321
- var _a, _b;
36451
+ var _a, _b, _c;
35322
36452
  this.clearConnectionReconcile();
35323
36453
  this.isResuming = false;
35324
36454
  this.bufferedEvents = [];
@@ -35335,6 +36465,9 @@ class Room extends eventsExports.EventEmitter {
35335
36465
  this.regionUrlProvider.notifyDisconnected();
35336
36466
  }
35337
36467
  try {
36468
+ // audio tracks that outlive this room are about to become unreachable through the local
36469
+ // participant, collect them while we can so they can be detached from the context below
36470
+ const retainedAudioTracks = shouldStopTracks ? [] : Array.from(this.localParticipant.audioTrackPublications.values()).map(pub => pub.track).filter(isLocalAudioTrack);
35338
36471
  this.remoteParticipants.forEach(p => {
35339
36472
  p.trackPublications.forEach(pub => {
35340
36473
  p.unpublishTrack(pub.trackSid);
@@ -35359,15 +36492,18 @@ class Room extends eventsExports.EventEmitter {
35359
36492
  this.remoteParticipants.clear();
35360
36493
  this.sidToIdentity.clear();
35361
36494
  this.activeSpeakers = [];
35362
- if (this.audioContext && typeof this.options.webAudioMix === 'boolean') {
35363
- this.audioContext.close();
35364
- this.audioContext = undefined;
35365
- }
36495
+ (_a = this.releaseDummyAudioElement) === null || _a === void 0 ? void 0 : _a.call(this);
36496
+ this.releaseAcquiredEmptyAudioTracks();
36497
+ // teardown is asynchronous (processors need to release their web audio nodes), but
36498
+ // `handleDisconnect` is not, so let it finish in the background
36499
+ this.releaseAudioContext(retainedAudioTracks).catch(error => this.log.warn('Could not release audio context', Object.assign(Object.assign({}, this.logContext), {
36500
+ error
36501
+ })));
35366
36502
  if (isWeb()) {
35367
36503
  window.removeEventListener('beforeunload', this.onPageLeave);
35368
36504
  window.removeEventListener('pagehide', this.onPageLeave);
35369
36505
  window.removeEventListener('freeze', this.onPageLeave);
35370
- (_b = (_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.removeEventListener) === null || _b === void 0 ? void 0 : _b.call(_a, 'devicechange', this.handleDeviceChange);
36506
+ (_c = (_b = navigator.mediaDevices) === null || _b === void 0 ? void 0 : _b.removeEventListener) === null || _c === void 0 ? void 0 : _c.call(_b, 'devicechange', this.handleDeviceChange);
35371
36507
  }
35372
36508
  } finally {
35373
36509
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
@@ -35442,39 +36578,139 @@ class Room extends eventsExports.EventEmitter {
35442
36578
  }
35443
36579
  });
35444
36580
  }
36581
+ /**
36582
+ * @param forConnect whether this acquisition belongs to a connect, which is the only caller
36583
+ * allowed to put a released room back on an audio context.
36584
+ */
35445
36585
  acquireAudioContext() {
35446
- return __awaiter(this, void 0, void 0, function* () {
35447
- var _a, _b;
35448
- if (typeof this.options.webAudioMix !== 'boolean' && this.options.webAudioMix.audioContext) {
35449
- // override audio context with custom audio context if supplied by user
35450
- this.audioContext = this.options.webAudioMix.audioContext;
35451
- } else if (!this.audioContext || this.audioContext.state === 'closed') {
35452
- // by using an AudioContext, it reduces lag on audio elements
35453
- // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
35454
- this.audioContext = (_a = getNewAudioContext()) !== null && _a !== void 0 ? _a : undefined;
35455
- }
35456
- if (this.options.webAudioMix) {
35457
- this.remoteParticipants.forEach(participant => participant.setAudioContext(this.audioContext));
35458
- }
35459
- this.localParticipant.setAudioContext(this.audioContext);
35460
- if (this.audioContext && this.audioContext.state === 'suspended') {
35461
- // for iOS a newly created AudioContext is always in `suspended` state.
35462
- // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
36586
+ return __awaiter(this, arguments, void 0, function () {
36587
+ var _this4 = this;
36588
+ let forConnect = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
36589
+ return function* () {
36590
+ var _a, _b;
36591
+ const unlock = yield _this4.audioContextMutex.lock();
35463
36592
  try {
35464
- yield Promise.race([this.audioContext.resume(), sleep(200)]);
35465
- } catch (e) {
35466
- this.log.warn('Could not resume audio context', {
35467
- error: e
35468
- });
36593
+ if (_this4.audioContextReleased && !forConnect) {
36594
+ // a disconnect set the flag and took the lock while this call was queued behind it.
36595
+ // Creating a context now would leave one open on a room nothing tears down again
36596
+ return;
36597
+ }
36598
+ _this4.audioContextReleased = false;
36599
+ const providedAudioContext = typeof _this4.options.webAudioMix !== 'boolean' ? _this4.options.webAudioMix.audioContext : undefined;
36600
+ if (providedAudioContext) {
36601
+ // override audio context with custom audio context if supplied by user
36602
+ yield _this4.setRoomAudioContext(providedAudioContext, false);
36603
+ } else if (!_this4.audioContext || _this4.audioContext.state === 'closed') {
36604
+ // by using an AudioContext, it reduces lag on audio elements
36605
+ // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
36606
+ yield _this4.setRoomAudioContext((_a = getNewAudioContext()) !== null && _a !== void 0 ? _a : undefined, true);
36607
+ }
36608
+ if (_this4.audioContext && _this4.audioContext.state === 'suspended') {
36609
+ // for iOS a newly created AudioContext is always in `suspended` state.
36610
+ // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
36611
+ try {
36612
+ yield Promise.race([_this4.audioContext.resume(), sleep(200)]);
36613
+ } catch (e) {
36614
+ _this4.log.warn('Could not resume audio context', {
36615
+ error: e
36616
+ });
36617
+ }
36618
+ }
36619
+ const newContextIsRunning = ((_b = _this4.audioContext) === null || _b === void 0 ? void 0 : _b.state) === 'running';
36620
+ if (newContextIsRunning !== _this4.canPlaybackAudio) {
36621
+ _this4.audioEnabled = newContextIsRunning;
36622
+ _this4.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
36623
+ }
36624
+ } finally {
36625
+ unlock();
35469
36626
  }
36627
+ }();
36628
+ });
36629
+ }
36630
+ /**
36631
+ * Points the room and its participants at `audioContext`, rebuilding the web audio graph of
36632
+ * every audio track in it. Closes the previous context if the SDK `owned` it.
36633
+ */
36634
+ setRoomAudioContext(audioContext, owned) {
36635
+ return __awaiter(this, void 0, void 0, function* () {
36636
+ const ownsAudioContext = !!audioContext && owned;
36637
+ if (this.audioContext === audioContext) {
36638
+ this.ownsAudioContext = ownsAudioContext;
36639
+ return;
35470
36640
  }
35471
- const newContextIsRunning = ((_b = this.audioContext) === null || _b === void 0 ? void 0 : _b.state) === 'running';
35472
- if (newContextIsRunning !== this.canPlaybackAudio) {
35473
- this.audioEnabled = newContextIsRunning;
35474
- this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
36641
+ const contextToClose = this.ownsAudioContext ? this.audioContext : undefined;
36642
+ this.audioContext = audioContext;
36643
+ this.ownsAudioContext = ownsAudioContext;
36644
+ try {
36645
+ // every audio track rebuilds its nodes in the new context, which has to happen before the
36646
+ // previous one is closed so that processors can move off it while it is still alive
36647
+ if (this.options.webAudioMix) {
36648
+ yield Promise.all(Array.from(this.remoteParticipants.values()).map(participant => participant.setAudioContext(audioContext)));
36649
+ }
36650
+ yield this.localParticipant.setAudioContext(audioContext);
36651
+ } catch (error) {
36652
+ // the context is being abandoned either way, so a track that failed to move off it must
36653
+ // not stop us closing it — that would leak the context this whole path exists to release
36654
+ this.log.warn('Could not move audio tracks to the new audio context', Object.assign(Object.assign({}, this.logContext), {
36655
+ error
36656
+ }));
36657
+ } finally {
36658
+ if (contextToClose && contextToClose.state !== 'closed') {
36659
+ try {
36660
+ yield contextToClose.close();
36661
+ } catch (error) {
36662
+ this.log.warn('Could not close audio context', Object.assign(Object.assign({}, this.logContext), {
36663
+ error
36664
+ }));
36665
+ }
36666
+ }
35475
36667
  }
35476
36668
  });
35477
36669
  }
36670
+ /**
36671
+ * Detaches the web audio graph the SDK built from the current audio context and closes that
36672
+ * context if the SDK created it. A context provided through `webAudioMix` is left untouched.
36673
+ */
36674
+ releaseAudioContext() {
36675
+ return __awaiter(this, arguments, void 0, function () {
36676
+ var _this5 = this;
36677
+ let retainedAudioTracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
36678
+ return function* () {
36679
+ // both reads happen before the first await, so they describe the room as it was when the
36680
+ // disconnect started rather than after a reconnect has had a chance to run
36681
+ _this5.audioContextReleased = true;
36682
+ const contextToRelease = _this5.audioContext;
36683
+ const unlock = yield _this5.audioContextMutex.lock();
36684
+ try {
36685
+ if (!_this5.audioContextReleased || _this5.audioContext !== contextToRelease) {
36686
+ // a reconnect acquired a new context while we were waiting, closing it would leave the
36687
+ // reconnected room without one
36688
+ return;
36689
+ }
36690
+ if (!_this5.ownsAudioContext) {
36691
+ // a provided context stays open, so tracks that outlive the room stay functional on it.
36692
+ // The tracks this room detached have released their nodes during track teardown already.
36693
+ // read under the lock: an acquire queued behind an in-flight release flips this flag
36694
+ return;
36695
+ }
36696
+ try {
36697
+ // tracks that outlive the room aren't reachable through the local participant anymore,
36698
+ // but still hold web audio nodes in the context we're about to close
36699
+ yield Promise.all(retainedAudioTracks.map(track => track.setAudioContext(undefined)));
36700
+ } catch (error) {
36701
+ _this5.log.warn('Could not detach retained audio tracks from the audio context', Object.assign(Object.assign({}, _this5.logContext), {
36702
+ error
36703
+ }));
36704
+ }
36705
+ // the participant maps have already been cleared by `handleDisconnect`, so this reaches no
36706
+ // tracks of its own — `retainedAudioTracks` above is what detaches them
36707
+ yield _this5.setRoomAudioContext(undefined, false);
36708
+ } finally {
36709
+ unlock();
36710
+ }
36711
+ }();
36712
+ });
36713
+ }
35478
36714
  createParticipant(identity, info) {
35479
36715
  var _a;
35480
36716
  let participant;
@@ -35490,7 +36726,9 @@ class Room extends eventsExports.EventEmitter {
35490
36726
  });
35491
36727
  }
35492
36728
  if (this.options.webAudioMix) {
35493
- participant.setAudioContext(this.audioContext);
36729
+ participant.setAudioContext(this.audioContext).catch(error => this.log.warn('Could not set audio context', Object.assign(Object.assign({}, this.logContext), {
36730
+ error
36731
+ })));
35494
36732
  }
35495
36733
  if ((_a = this.options.audioOutput) === null || _a === void 0 ? void 0 : _a.deviceId) {
35496
36734
  participant.setAudioOutput(this.options.audioOutput).catch(e => this.log.warn("Could not set audio output: ".concat(e.message)));
@@ -35603,28 +36841,28 @@ class Room extends eventsExports.EventEmitter {
35603
36841
  }
35604
36842
  // Advertise deflate-raw decompression support so peers know they can send us compressed data
35605
36843
  // streams (gated separately from clientProtocol — see the data streams v2 spec).
35606
- if (isCompressionStreamSupported()) {
36844
+ if (isDeflateRawCompressionSupported()) {
35607
36845
  capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
35608
36846
  }
35609
36847
  return capabilities;
35610
36848
  }
35611
36849
  registerRpcDataStreamHandler() {
35612
36850
  this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
35613
- var _this4 = this;
36851
+ var _this6 = this;
35614
36852
  let identity = _ref7.identity;
35615
36853
  return function* () {
35616
36854
  var _b;
35617
36855
  const attributes = (_b = reader.info.attributes) !== null && _b !== void 0 ? _b : {};
35618
- yield _this4.rpcServerManager.handleIncomingDataStream(reader, identity, attributes);
36856
+ yield _this6.rpcServerManager.handleIncomingDataStream(reader, identity, attributes);
35619
36857
  }();
35620
36858
  }));
35621
36859
  this.incomingDataStreamManager.registerTextStreamHandler(RPC_RESPONSE_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref8) {
35622
- var _this5 = this;
36860
+ var _this7 = this;
35623
36861
  let identity = _ref8.identity;
35624
36862
  return function* () {
35625
36863
  var _b;
35626
36864
  const attributes = (_b = reader.info.attributes) !== null && _b !== void 0 ? _b : {};
35627
- yield _this5.rpcClientManager.handleIncomingDataStream(reader, identity, attributes);
36865
+ yield _this7.rpcClientManager.handleIncomingDataStream(reader, identity, attributes);
35628
36866
  }();
35629
36867
  }));
35630
36868
  }
@@ -35783,7 +37021,7 @@ class Room extends eventsExports.EventEmitter {
35783
37021
  type: TrackType.AUDIO
35784
37022
  }), new LocalAudioTrack(publishOptions.useRealTracks && ((_c = navigator.mediaDevices) === null || _c === void 0 ? void 0 : _c.getUserMedia) ? (yield navigator.mediaDevices.getUserMedia({
35785
37023
  audio: true
35786
- })).getAudioTracks()[0] : getEmptyAudioStreamTrack(), undefined, false, this.audioContext, {
37024
+ })).getAudioTracks()[0] : this.acquireEmptyAudioStreamTrack(), undefined, false, this.audioContext, {
35787
37025
  loggerName: this.options.loggerName,
35788
37026
  loggerContextCb: () => this.logContext
35789
37027
  }), {
@@ -35814,7 +37052,7 @@ class Room extends eventsExports.EventEmitter {
35814
37052
  info.tracks = [...info.tracks, videoTrack];
35815
37053
  }
35816
37054
  if (participantOptions.audio) {
35817
- const dummyTrack = getEmptyAudioStreamTrack();
37055
+ const dummyTrack = this.acquireEmptyAudioStreamTrack();
35818
37056
  const audioTrack = new TrackInfo({
35819
37057
  source: TrackSource.MICROPHONE,
35820
37058
  sid: Math.floor(Math.random() * 10000).toString(),
@@ -36653,225 +37891,186 @@ class TokenSourceFixed {}
36653
37891
  * and {@link TokenSourceCustom}.
36654
37892
  */
36655
37893
  class TokenSourceConfigurable {}new TextEncoder();
36656
- const decoder = new TextDecoder();
36657
- const strictDecoder = new TextDecoder('utf-8', {
36658
- fatal: true
36659
- });class JOSEError extends Error {
37894
+ const decoder = new TextDecoder(),
37895
+ strictDecoder = new TextDecoder("utf-8", {
37896
+ fatal: true
37897
+ });
37898
+ function decodeBase64(encoded) {
37899
+ let url = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
37900
+ if (Uint8Array.fromBase64) return Uint8Array.fromBase64(encoded, {
37901
+ alphabet: url ? "base64url" : "base64"
37902
+ });
37903
+ if (url) {
37904
+ if (encoded.includes("+") || encoded.includes("/")) throw new TypeError("Invalid base64url");
37905
+ encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
37906
+ }
37907
+ const binary = atob(encoded),
37908
+ bytes = new Uint8Array(binary.length);
37909
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
37910
+ return bytes;
37911
+ }class JOSEError extends Error {
36660
37912
  constructor(message, options) {
36661
37913
  var _Error$captureStackTr;
36662
- super(message, options);
36663
- _defineProperty(this, "code", 'ERR_JOSE_GENERIC');
36664
- this.name = this.constructor.name;
36665
- (_Error$captureStackTr = Error.captureStackTrace) === null || _Error$captureStackTr === void 0 || _Error$captureStackTr.call(Error, this, this.constructor);
37914
+ super(message, options), _defineProperty(this, "code", "ERR_JOSE_GENERIC"), this.name = this.constructor.name, (_Error$captureStackTr = Error.captureStackTrace) === null || _Error$captureStackTr === void 0 ? void 0 : _Error$captureStackTr.call(Error, this, this.constructor);
36666
37915
  }
36667
37916
  }
36668
- _defineProperty(JOSEError, "code", 'ERR_JOSE_GENERIC');
37917
+ _defineProperty(JOSEError, "code", "ERR_JOSE_GENERIC");
36669
37918
  class JWTClaimValidationFailed extends JOSEError {
36670
37919
  constructor(message, payload) {
36671
- let claim = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'unspecified';
36672
- let reason = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'unspecified';
37920
+ let claim = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "unspecified";
37921
+ let reason = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "unspecified";
36673
37922
  super(message, {
36674
37923
  cause: {
36675
37924
  claim,
36676
37925
  reason,
36677
37926
  payload
36678
37927
  }
36679
- });
36680
- _defineProperty(this, "code", 'ERR_JWT_CLAIM_VALIDATION_FAILED');
36681
- _defineProperty(this, "claim", void 0);
36682
- _defineProperty(this, "reason", void 0);
36683
- _defineProperty(this, "payload", void 0);
36684
- this.claim = claim;
36685
- this.reason = reason;
36686
- this.payload = payload;
37928
+ }), _defineProperty(this, "code", "ERR_JWT_CLAIM_VALIDATION_FAILED"), _defineProperty(this, "claim", void 0), _defineProperty(this, "reason", void 0), _defineProperty(this, "payload", void 0), this.claim = claim, this.reason = reason, this.payload = payload;
36687
37929
  }
36688
37930
  }
36689
- _defineProperty(JWTClaimValidationFailed, "code", 'ERR_JWT_CLAIM_VALIDATION_FAILED');
37931
+ _defineProperty(JWTClaimValidationFailed, "code", "ERR_JWT_CLAIM_VALIDATION_FAILED");
36690
37932
  class JWTExpired extends JOSEError {
36691
37933
  constructor(message, payload) {
36692
- let claim = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'unspecified';
36693
- let reason = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'unspecified';
37934
+ let claim = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "unspecified";
37935
+ let reason = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "unspecified";
36694
37936
  super(message, {
36695
37937
  cause: {
36696
37938
  claim,
36697
37939
  reason,
36698
37940
  payload
36699
37941
  }
36700
- });
36701
- _defineProperty(this, "code", 'ERR_JWT_EXPIRED');
36702
- _defineProperty(this, "claim", void 0);
36703
- _defineProperty(this, "reason", void 0);
36704
- _defineProperty(this, "payload", void 0);
36705
- this.claim = claim;
36706
- this.reason = reason;
36707
- this.payload = payload;
37942
+ }), _defineProperty(this, "code", "ERR_JWT_EXPIRED"), _defineProperty(this, "claim", void 0), _defineProperty(this, "reason", void 0), _defineProperty(this, "payload", void 0), this.claim = claim, this.reason = reason, this.payload = payload;
36708
37943
  }
36709
37944
  }
36710
- _defineProperty(JWTExpired, "code", 'ERR_JWT_EXPIRED');
37945
+ _defineProperty(JWTExpired, "code", "ERR_JWT_EXPIRED");
36711
37946
  class JOSEAlgNotAllowed extends JOSEError {
36712
37947
  constructor() {
36713
37948
  super(...arguments);
36714
- _defineProperty(this, "code", 'ERR_JOSE_ALG_NOT_ALLOWED');
37949
+ _defineProperty(this, "code", "ERR_JOSE_ALG_NOT_ALLOWED");
36715
37950
  }
36716
37951
  }
36717
- _defineProperty(JOSEAlgNotAllowed, "code", 'ERR_JOSE_ALG_NOT_ALLOWED');
37952
+ _defineProperty(JOSEAlgNotAllowed, "code", "ERR_JOSE_ALG_NOT_ALLOWED");
36718
37953
  class JOSENotSupported extends JOSEError {
36719
37954
  constructor() {
36720
37955
  super(...arguments);
36721
- _defineProperty(this, "code", 'ERR_JOSE_NOT_SUPPORTED');
37956
+ _defineProperty(this, "code", "ERR_JOSE_NOT_SUPPORTED");
36722
37957
  }
36723
37958
  }
36724
- _defineProperty(JOSENotSupported, "code", 'ERR_JOSE_NOT_SUPPORTED');
37959
+ _defineProperty(JOSENotSupported, "code", "ERR_JOSE_NOT_SUPPORTED");
36725
37960
  class JWEDecryptionFailed extends JOSEError {
36726
37961
  constructor() {
36727
- let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'decryption operation failed';
37962
+ let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "decryption operation failed";
36728
37963
  let options = arguments.length > 1 ? arguments[1] : undefined;
36729
37964
  super(message, options);
36730
- _defineProperty(this, "code", 'ERR_JWE_DECRYPTION_FAILED');
37965
+ _defineProperty(this, "code", "ERR_JWE_DECRYPTION_FAILED");
36731
37966
  }
36732
37967
  }
36733
- _defineProperty(JWEDecryptionFailed, "code", 'ERR_JWE_DECRYPTION_FAILED');
37968
+ _defineProperty(JWEDecryptionFailed, "code", "ERR_JWE_DECRYPTION_FAILED");
36734
37969
  class JWEInvalid extends JOSEError {
36735
37970
  constructor() {
36736
37971
  super(...arguments);
36737
- _defineProperty(this, "code", 'ERR_JWE_INVALID');
37972
+ _defineProperty(this, "code", "ERR_JWE_INVALID");
36738
37973
  }
36739
37974
  }
36740
- _defineProperty(JWEInvalid, "code", 'ERR_JWE_INVALID');
37975
+ _defineProperty(JWEInvalid, "code", "ERR_JWE_INVALID");
36741
37976
  class JWSInvalid extends JOSEError {
36742
37977
  constructor() {
36743
37978
  super(...arguments);
36744
- _defineProperty(this, "code", 'ERR_JWS_INVALID');
37979
+ _defineProperty(this, "code", "ERR_JWS_INVALID");
36745
37980
  }
36746
37981
  }
36747
- _defineProperty(JWSInvalid, "code", 'ERR_JWS_INVALID');
37982
+ _defineProperty(JWSInvalid, "code", "ERR_JWS_INVALID");
36748
37983
  class JWTInvalid extends JOSEError {
36749
37984
  constructor() {
36750
37985
  super(...arguments);
36751
- _defineProperty(this, "code", 'ERR_JWT_INVALID');
37986
+ _defineProperty(this, "code", "ERR_JWT_INVALID");
36752
37987
  }
36753
37988
  }
36754
- _defineProperty(JWTInvalid, "code", 'ERR_JWT_INVALID');
37989
+ _defineProperty(JWTInvalid, "code", "ERR_JWT_INVALID");
36755
37990
  class JWKInvalid extends JOSEError {
36756
37991
  constructor() {
36757
37992
  super(...arguments);
36758
- _defineProperty(this, "code", 'ERR_JWK_INVALID');
37993
+ _defineProperty(this, "code", "ERR_JWK_INVALID");
36759
37994
  }
36760
37995
  }
36761
- _defineProperty(JWKInvalid, "code", 'ERR_JWK_INVALID');
37996
+ _defineProperty(JWKInvalid, "code", "ERR_JWK_INVALID");
36762
37997
  class JWKSInvalid extends JOSEError {
36763
37998
  constructor() {
36764
37999
  super(...arguments);
36765
- _defineProperty(this, "code", 'ERR_JWKS_INVALID');
38000
+ _defineProperty(this, "code", "ERR_JWKS_INVALID");
36766
38001
  }
36767
38002
  }
36768
- _defineProperty(JWKSInvalid, "code", 'ERR_JWKS_INVALID');
38003
+ _defineProperty(JWKSInvalid, "code", "ERR_JWKS_INVALID");
36769
38004
  class JWKSNoMatchingKey extends JOSEError {
36770
38005
  constructor() {
36771
- let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'no applicable key found in the JSON Web Key Set';
38006
+ let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "no applicable key found in the JSON Web Key Set";
36772
38007
  let options = arguments.length > 1 ? arguments[1] : undefined;
36773
38008
  super(message, options);
36774
- _defineProperty(this, "code", 'ERR_JWKS_NO_MATCHING_KEY');
38009
+ _defineProperty(this, "code", "ERR_JWKS_NO_MATCHING_KEY");
36775
38010
  }
36776
38011
  }
36777
- _defineProperty(JWKSNoMatchingKey, "code", 'ERR_JWKS_NO_MATCHING_KEY');
38012
+ _defineProperty(JWKSNoMatchingKey, "code", "ERR_JWKS_NO_MATCHING_KEY");
36778
38013
  class JWKSMultipleMatchingKeys extends JOSEError {
36779
38014
  constructor() {
36780
- let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'multiple matching keys found in the JSON Web Key Set';
38015
+ let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "multiple matching keys found in the JSON Web Key Set";
36781
38016
  let options = arguments.length > 1 ? arguments[1] : undefined;
36782
38017
  super(message, options);
36783
38018
  _defineProperty(this, Symbol.asyncIterator, _wrapAsyncGenerator(function* () {}));
36784
- _defineProperty(this, "code", 'ERR_JWKS_MULTIPLE_MATCHING_KEYS');
38019
+ _defineProperty(this, "code", "ERR_JWKS_MULTIPLE_MATCHING_KEYS");
36785
38020
  }
36786
38021
  }
36787
- _defineProperty(JWKSMultipleMatchingKeys, "code", 'ERR_JWKS_MULTIPLE_MATCHING_KEYS');
38022
+ _defineProperty(JWKSMultipleMatchingKeys, "code", "ERR_JWKS_MULTIPLE_MATCHING_KEYS");
36788
38023
  class JWKSTimeout extends JOSEError {
36789
38024
  constructor() {
36790
- let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'request timed out';
38025
+ let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "request timed out";
36791
38026
  let options = arguments.length > 1 ? arguments[1] : undefined;
36792
38027
  super(message, options);
36793
- _defineProperty(this, "code", 'ERR_JWKS_TIMEOUT');
38028
+ _defineProperty(this, "code", "ERR_JWKS_TIMEOUT");
36794
38029
  }
36795
38030
  }
36796
- _defineProperty(JWKSTimeout, "code", 'ERR_JWKS_TIMEOUT');
38031
+ _defineProperty(JWKSTimeout, "code", "ERR_JWKS_TIMEOUT");
36797
38032
  class JWSSignatureVerificationFailed extends JOSEError {
36798
38033
  constructor() {
36799
- let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'signature verification failed';
38034
+ let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "signature verification failed";
36800
38035
  let options = arguments.length > 1 ? arguments[1] : undefined;
36801
38036
  super(message, options);
36802
- _defineProperty(this, "code", 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED');
38037
+ _defineProperty(this, "code", "ERR_JWS_SIGNATURE_VERIFICATION_FAILED");
36803
38038
  }
36804
38039
  }
36805
- _defineProperty(JWSSignatureVerificationFailed, "code", 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED');function decodeBase64(encoded) {
36806
- if (Uint8Array.fromBase64) {
36807
- return Uint8Array.fromBase64(encoded);
36808
- }
36809
- const binary = atob(encoded);
36810
- const bytes = new Uint8Array(binary.length);
36811
- for (let i = 0; i < binary.length; i++) {
36812
- bytes[i] = binary.charCodeAt(i);
36813
- }
36814
- return bytes;
36815
- }const invalid = 'The input to be decoded is not correctly encoded.';
38040
+ _defineProperty(JWSSignatureVerificationFailed, "code", "ERR_JWS_SIGNATURE_VERIFICATION_FAILED");const invalid = "The input to be decoded is not correctly encoded.";
36816
38041
  function decode(input) {
36817
- if (Uint8Array.fromBase64) {
36818
- try {
36819
- return Uint8Array.fromBase64(typeof input === 'string' ? input : decoder.decode(input), {
36820
- alphabet: 'base64url'
36821
- });
36822
- } catch (cause) {
36823
- throw new TypeError(invalid, {
36824
- cause
36825
- });
36826
- }
36827
- }
36828
- let encoded = input;
36829
- if (encoded instanceof Uint8Array) {
36830
- encoded = decoder.decode(encoded);
36831
- }
36832
- if (encoded.includes('+') || encoded.includes('/')) {
36833
- throw new TypeError(invalid);
36834
- }
36835
- encoded = encoded.replace(/-/g, '+').replace(/_/g, '/');
36836
38042
  try {
36837
- return decodeBase64(encoded);
36838
- } catch (_unused) {
36839
- throw new TypeError(invalid);
38043
+ return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0);
38044
+ } catch (cause) {
38045
+ throw new TypeError(invalid, {
38046
+ cause
38047
+ });
36840
38048
  }
36841
38049
  }function isObject(input) {
36842
- if (typeof input !== 'object' || input === null || Object.prototype.toString.call(input) !== '[object Object]') {
36843
- return false;
36844
- }
38050
+ if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") return false;
36845
38051
  const prototype = Object.getPrototypeOf(input);
36846
- if (prototype === null) {
36847
- return true;
36848
- }
36849
- let proto = prototype;
36850
- while (Object.getPrototypeOf(proto) !== null) {
36851
- proto = Object.getPrototypeOf(proto);
36852
- }
36853
- return prototype === proto;
38052
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
36854
38053
  }function decodeJwt(jwt) {
36855
- if (typeof jwt !== 'string') throw new JWTInvalid('JWTs must use Compact JWS serialization, JWT must be a string');
36856
- const _jwt$split = jwt.split('.'),
38054
+ if (typeof jwt != "string") throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string");
38055
+ const _jwt$split = jwt.split("."),
36857
38056
  payload = _jwt$split[1],
36858
38057
  length = _jwt$split.length;
36859
- if (length === 5) throw new JWTInvalid('Only JWTs using Compact JWS serialization can be decoded');
36860
- if (length !== 3) throw new JWTInvalid('Invalid JWT');
36861
- if (!payload) throw new JWTInvalid('JWTs must contain a payload');
38058
+ if (length === 5) throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded");
38059
+ if (length !== 3) throw new JWTInvalid("Invalid JWT");
38060
+ if (!payload) throw new JWTInvalid("JWTs must contain a payload");
36862
38061
  let decoded;
36863
38062
  try {
36864
38063
  decoded = decode(payload);
36865
38064
  } catch (_unused) {
36866
- throw new JWTInvalid('Failed to base64url decode the payload');
38065
+ throw new JWTInvalid("Failed to base64url decode the payload");
36867
38066
  }
36868
38067
  let result;
36869
38068
  try {
36870
38069
  result = JSON.parse(strictDecoder.decode(decoded));
36871
38070
  } catch (_unused2) {
36872
- throw new JWTInvalid('Failed to parse the decoded payload as JSON');
38071
+ throw new JWTInvalid("Failed to parse the decoded payload as JSON");
36873
38072
  }
36874
- if (!isObject(result)) throw new JWTInvalid('Invalid JWT Claims Set');
38073
+ if (!isObject(result)) throw new JWTInvalid("Invalid JWT Claims Set");
36875
38074
  return result;
36876
38075
  }const ONE_SECOND_IN_MILLISECONDS = 1000;
36877
38076
  const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
@@ -37279,4 +38478,4 @@ const serializers = {
37279
38478
  json,
37280
38479
  raw,
37281
38480
  custom
37282
- };export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
38481
+ };export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,canFailOverToAnotherRegion,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,releaseEmptyAudioStreamTrack,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map