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
package/src/room/Room.ts CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  ConnectionErrorReason,
78
78
  UnexpectedConnectionState,
79
79
  UnsupportedServer,
80
+ canFailOverToAnotherRegion,
80
81
  } from './errors';
81
82
  import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
82
83
  import LocalParticipant from './participant/LocalParticipant';
@@ -120,14 +121,16 @@ import {
120
121
  getEmptyAudioStreamTrack,
121
122
  isBrowserSupported,
122
123
  isCloud,
123
- isCompressionStreamSupported,
124
+ isDeflateRawCompressionSupported,
124
125
  isLocalAudioTrack,
125
126
  isLocalParticipant,
127
+ isLocalVideoTrack,
126
128
  isReactNative,
127
129
  isRemotePub,
128
130
  isSafariBased,
129
131
  isWeb,
130
132
  numberToBigInt,
133
+ releaseEmptyAudioStreamTrack,
131
134
  sleep,
132
135
  supportsSetSinkId,
133
136
  toHttpUrl,
@@ -146,6 +149,30 @@ export enum ConnectionState {
146
149
  const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
147
150
  const STATS_LOG_FREQUENCY_MS = 30 * 1000;
148
151
 
152
+ const DUMMY_AUDIO_ELEMENT_ID = 'livekit-dummy-audio-el';
153
+
154
+ /**
155
+ * One silent audio element serves every room on the page, so it is reference counted: a room
156
+ * disconnecting must not pull it out from under the rooms still playing through it. The element
157
+ * and its track go away once the last room hands its reference back.
158
+ */
159
+ let sharedDummyAudioElement:
160
+ | { element: HTMLAudioElement; stream: MediaStream; track: MediaStreamTrack; refCount: number }
161
+ | undefined;
162
+
163
+ function createSharedDummyAudioElement() {
164
+ const element = document.createElement('audio');
165
+ element.id = DUMMY_AUDIO_ELEMENT_ID;
166
+ element.autoplay = true;
167
+ element.hidden = true;
168
+ const track = getEmptyAudioStreamTrack();
169
+ track.enabled = true;
170
+ const stream = new MediaStream([track]);
171
+ element.srcObject = stream;
172
+ document.body.append(element);
173
+ return { element, stream, track, refCount: 0 };
174
+ }
175
+
149
176
  /**
150
177
  * In LiveKit, a room is the logical grouping for a list of participants.
151
178
  * Participants in a room can publish tracks, and subscribe to others' tracks.
@@ -193,6 +220,31 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
193
220
 
194
221
  private audioContext?: AudioContext;
195
222
 
223
+ /**
224
+ * whether `audioContext` was created by the SDK and therefore has to be closed by it.
225
+ * A context handed in through `webAudioMix` is owned by the application instead.
226
+ */
227
+ private ownsAudioContext = false;
228
+
229
+ /**
230
+ * set once the audio context has been released as part of a disconnect. Guards against
231
+ * creating a new context outside of a connection lifecycle, where nothing would close it again.
232
+ */
233
+ private audioContextReleased = false;
234
+
235
+ /**
236
+ * serialises `acquireAudioContext` against `releaseAudioContext`. Both await per-track graph
237
+ * rebuilds, so without it a reconnect landing inside a disconnect's teardown leaves the two
238
+ * racing to set the participants' context, and the loser's value is the one that sticks.
239
+ */
240
+ private audioContextMutex: Mutex = new Mutex();
241
+
242
+ /** tears down the iOS dummy audio element, set while one is in place. See `startAudio` */
243
+ private releaseDummyAudioElement?: () => void;
244
+
245
+ /** silent audio tracks this room acquired, handed back to the shared source on disconnect */
246
+ private acquiredEmptyAudioTracks = new Set<MediaStreamTrack>();
247
+
196
248
  /** used for aborting pending connections to a LiveKit server */
197
249
  private abortController?: AbortController;
198
250
 
@@ -755,6 +807,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
755
807
  updatedAtInMs: Date.now(),
756
808
  maxAgeInMs: DEFAULT_MAX_AGE_MS,
757
809
  });
810
+ })
811
+ .on(EngineEvent.RequestSubscribedCodecRefresh, () => {
812
+ for (const videoPub of this.localParticipant.videoTrackPublications.values()) {
813
+ if (isLocalVideoTrack(videoPub.track)) {
814
+ videoPub.track.refreshSubscribedCodecs();
815
+ }
816
+ }
758
817
  });
759
818
 
760
819
  if (this.localParticipant) {
@@ -908,8 +967,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
908
967
  if (
909
968
  this.regionUrlProvider &&
910
969
  error instanceof ConnectionError &&
911
- error.reason !== ConnectionErrorReason.Cancelled &&
912
- error.reason !== ConnectionErrorReason.NotAllowed
970
+ canFailOverToAnotherRegion(error)
913
971
  ) {
914
972
  let nextUrl: string | null = null;
915
973
  try {
@@ -992,6 +1050,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
992
1050
  autoSubscribe: connectOptions.autoSubscribe,
993
1051
  adaptiveStream:
994
1052
  typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
1053
+ disableIceLite: connectOptions.disableIceLite,
1054
+ subscriberAllowPause: connectOptions.subscriberAllowPause,
995
1055
  clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
996
1056
  maxRetries: connectOptions.maxRetries,
997
1057
  e2eeEnabled: !!this.e2eeManager,
@@ -1063,7 +1123,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1063
1123
  this.engine.setRegionStrategy(this.createRegionStrategy());
1064
1124
  }
1065
1125
 
1066
- this.acquireAudioContext();
1126
+ this.acquireAudioContext(true).catch((error) =>
1127
+ this.log.warn('Could not acquire audio context', { ...this.logContext, error }),
1128
+ );
1067
1129
 
1068
1130
  this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
1069
1131
 
@@ -1211,8 +1273,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1211
1273
  let req: SimulateScenario | undefined;
1212
1274
  switch (scenario) {
1213
1275
  case 'signal-reconnect':
1276
+ const reconnectDelay = typeof arg === 'number' ? arg : 0;
1214
1277
  // @ts-expect-error function is private
1215
- await this.engine.client.handleOnClose('simulate disconnect');
1278
+ await this.engine.client.handleOnClose('simulate disconnect', undefined, reconnectDelay);
1216
1279
  break;
1217
1280
  case 'fail-on-v1-path':
1218
1281
  this.engine.failNextV1Path();
@@ -1334,6 +1397,93 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1334
1397
  await this.disconnect();
1335
1398
  };
1336
1399
 
1400
+ /**
1401
+ * iOS blocks audio element playback if
1402
+ * - user is not publishing audio themselves and
1403
+ * - no other audio source is playing
1404
+ *
1405
+ * as a workaround, we create an audio element with an empty track, so that
1406
+ * silent audio is always playing
1407
+ *
1408
+ * The element is shared with any other room on the page and handed back on disconnect, so it
1409
+ * outlives whichever room happened to create it.
1410
+ */
1411
+ private acquireDummyAudioElement(): HTMLAudioElement {
1412
+ if (!sharedDummyAudioElement) {
1413
+ sharedDummyAudioElement = createSharedDummyAudioElement();
1414
+ }
1415
+ const shared = sharedDummyAudioElement;
1416
+ if (this.releaseDummyAudioElement) {
1417
+ // this room already holds a reference, a second one would never be handed back
1418
+ return shared.element;
1419
+ }
1420
+ shared.refCount += 1;
1421
+
1422
+ // the listener is per room rather than shared, so that the room it resumes playback for is
1423
+ // still connected, and so that it goes away with the room that installed it
1424
+ const handleVisibilityChange = () => {
1425
+ // set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
1426
+ shared.element.srcObject = document.hidden ? null : shared.stream;
1427
+ if (!document.hidden) {
1428
+ this.log.debug(
1429
+ 'page visible again, triggering startAudio to resume playback and update playback status',
1430
+ );
1431
+ this.startAudio().catch((error) =>
1432
+ this.log.warn('Could not restart audio on page becoming visible', {
1433
+ ...this.logContext,
1434
+ error,
1435
+ }),
1436
+ );
1437
+ }
1438
+ };
1439
+ document.addEventListener('visibilitychange', handleVisibilityChange);
1440
+
1441
+ this.releaseDummyAudioElement = () => {
1442
+ this.releaseDummyAudioElement = undefined;
1443
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
1444
+ shared.refCount -= 1;
1445
+ if (shared.refCount > 0) {
1446
+ return;
1447
+ }
1448
+ shared.element.srcObject = null;
1449
+ shared.element.remove();
1450
+ sharedDummyAudioElement = undefined;
1451
+ releaseEmptyAudioStreamTrack(shared.track).catch((error) =>
1452
+ this.log.warn('Could not release the dummy audio element track', {
1453
+ ...this.logContext,
1454
+ error,
1455
+ }),
1456
+ );
1457
+ };
1458
+
1459
+ return shared.element;
1460
+ }
1461
+
1462
+ /**
1463
+ * Obtains a silent audio track from the shared source and keeps track of it, so that it can be
1464
+ * handed back on disconnect and the shared `AudioContext` behind it can be closed again.
1465
+ */
1466
+ private acquireEmptyAudioStreamTrack(): MediaStreamTrack {
1467
+ const track = getEmptyAudioStreamTrack();
1468
+ this.acquiredEmptyAudioTracks.add(track);
1469
+ return track;
1470
+ }
1471
+
1472
+ private releaseAcquiredEmptyAudioTrack(track: MediaStreamTrack) {
1473
+ if (!this.acquiredEmptyAudioTracks.delete(track)) {
1474
+ return;
1475
+ }
1476
+ releaseEmptyAudioStreamTrack(track).catch((error) =>
1477
+ this.log.warn('Could not release empty audio stream track', { ...this.logContext, error }),
1478
+ );
1479
+ }
1480
+
1481
+ private releaseAcquiredEmptyAudioTracks() {
1482
+ for (const track of this.acquiredEmptyAudioTracks) {
1483
+ this.releaseAcquiredEmptyAudioTrack(track);
1484
+ }
1485
+ }
1486
+
1337
1487
  /**
1338
1488
  * Browsers have different policies regarding audio playback. Most requiring
1339
1489
  * some form of user interaction (click/tap/etc).
@@ -1344,46 +1494,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1344
1494
  startAudio = async () => {
1345
1495
  const elements: Array<HTMLMediaElement> = [];
1346
1496
  const browser = getBrowser();
1347
- if (browser && browser.os === 'iOS') {
1348
- /**
1349
- * iOS blocks audio element playback if
1350
- * - user is not publishing audio themselves and
1351
- * - no other audio source is playing
1352
- *
1353
- * as a workaround, we create an audio element with an empty track, so that
1354
- * silent audio is always playing
1355
- */
1356
- const audioId = 'livekit-dummy-audio-el';
1357
- let dummyAudioEl = document.getElementById(audioId) as HTMLAudioElement | null;
1358
- if (!dummyAudioEl) {
1359
- dummyAudioEl = document.createElement('audio');
1360
- dummyAudioEl.id = audioId;
1361
- dummyAudioEl.autoplay = true;
1362
- dummyAudioEl.hidden = true;
1363
- const track = getEmptyAudioStreamTrack();
1364
- track.enabled = true;
1365
- const stream = new MediaStream([track]);
1366
- dummyAudioEl.srcObject = stream;
1367
- document.addEventListener('visibilitychange', () => {
1368
- if (!dummyAudioEl) {
1369
- return;
1370
- }
1371
- // set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
1372
- dummyAudioEl.srcObject = document.hidden ? null : stream;
1373
- if (!document.hidden) {
1374
- this.log.debug(
1375
- 'page visible again, triggering startAudio to resume playback and update playback status',
1376
- );
1377
- this.startAudio();
1378
- }
1379
- });
1380
- document.body.append(dummyAudioEl);
1381
- this.once(RoomEvent.Disconnected, () => {
1382
- dummyAudioEl?.remove();
1383
- dummyAudioEl = null;
1384
- });
1385
- }
1386
- elements.push(dummyAudioEl);
1497
+ if (browser && browser.os === 'iOS' && !this.audioContextReleased) {
1498
+ elements.push(this.acquireDummyAudioElement());
1387
1499
  }
1388
1500
 
1389
1501
  this.remoteParticipants.forEach((p) => {
@@ -1398,7 +1510,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1398
1510
 
1399
1511
  try {
1400
1512
  await Promise.all([
1401
- this.acquireAudioContext(),
1513
+ // once the audio context has been released as part of a disconnect there is nothing left
1514
+ // to unlock, and creating a new context here would leak it as no disconnect will follow
1515
+ ...(this.audioContextReleased ? [] : [this.acquireAudioContext()]),
1402
1516
  ...elements.map((e) => {
1403
1517
  // when webAudioMix is enabled, attached elements are deliberately kept muted by
1404
1518
  // RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
@@ -1848,6 +1962,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1848
1962
  }
1849
1963
 
1850
1964
  try {
1965
+ // audio tracks that outlive this room are about to become unreachable through the local
1966
+ // participant, collect them while we can so they can be detached from the context below
1967
+ const retainedAudioTracks = shouldStopTracks
1968
+ ? []
1969
+ : Array.from(this.localParticipant.audioTrackPublications.values())
1970
+ .map((pub) => pub.track)
1971
+ .filter(isLocalAudioTrack);
1972
+
1851
1973
  this.remoteParticipants.forEach((p) => {
1852
1974
  p.trackPublications.forEach((pub) => {
1853
1975
  p.unpublishTrack(pub.trackSid);
@@ -1890,10 +2012,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1890
2012
  this.remoteParticipants.clear();
1891
2013
  this.sidToIdentity.clear();
1892
2014
  this.activeSpeakers = [];
1893
- if (this.audioContext && typeof this.options.webAudioMix === 'boolean') {
1894
- this.audioContext.close();
1895
- this.audioContext = undefined;
1896
- }
2015
+ this.releaseDummyAudioElement?.();
2016
+ this.releaseAcquiredEmptyAudioTracks();
2017
+ // teardown is asynchronous (processors need to release their web audio nodes), but
2018
+ // `handleDisconnect` is not, so let it finish in the background
2019
+ this.releaseAudioContext(retainedAudioTracks).catch((error) =>
2020
+ this.log.warn('Could not release audio context', { ...this.logContext, error }),
2021
+ );
1897
2022
  if (isWeb()) {
1898
2023
  window.removeEventListener('beforeunload', this.onPageLeave);
1899
2024
  window.removeEventListener('pagehide', this.onPageLeave);
@@ -2097,7 +2222,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2097
2222
  // find the participant
2098
2223
  const participant = this.remoteParticipants.get(packet.participantIdentity);
2099
2224
  if (packet.value.case === 'user') {
2100
- this.handleUserPacket(participant, packet.value.value, packet.kind, encryptionType);
2225
+ this.handleUserPacket(
2226
+ participant,
2227
+ packet.value.value,
2228
+ packet.kind,
2229
+ encryptionType,
2230
+ packet.participantIdentity,
2231
+ );
2101
2232
  } else if (packet.value.case === 'transcription') {
2102
2233
  this.handleTranscription(participant, packet.value.value);
2103
2234
  } else if (packet.value.case === 'sipDtmf') {
@@ -2147,6 +2278,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2147
2278
  userPacket: UserPacket,
2148
2279
  kind: DataPacket_Kind,
2149
2280
  encryptionType: Encryption_Type,
2281
+ participantIdentity: string,
2150
2282
  ) => {
2151
2283
  this.emit(
2152
2284
  RoomEvent.DataReceived,
@@ -2155,6 +2287,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2155
2287
  kind,
2156
2288
  userPacket.topic,
2157
2289
  encryptionType,
2290
+ participantIdentity,
2158
2291
  );
2159
2292
 
2160
2293
  // also emit on the participant
@@ -2340,38 +2473,132 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2340
2473
  });
2341
2474
  };
2342
2475
 
2343
- private async acquireAudioContext() {
2344
- if (typeof this.options.webAudioMix !== 'boolean' && this.options.webAudioMix.audioContext) {
2345
- // override audio context with custom audio context if supplied by user
2346
- this.audioContext = this.options.webAudioMix.audioContext;
2347
- } else if (!this.audioContext || this.audioContext.state === 'closed') {
2348
- // by using an AudioContext, it reduces lag on audio elements
2349
- // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
2350
- this.audioContext = getNewAudioContext() ?? undefined;
2351
- }
2476
+ /**
2477
+ * @param forConnect whether this acquisition belongs to a connect, which is the only caller
2478
+ * allowed to put a released room back on an audio context.
2479
+ */
2480
+ private async acquireAudioContext(forConnect = false) {
2481
+ const unlock = await this.audioContextMutex.lock();
2482
+ try {
2483
+ if (this.audioContextReleased && !forConnect) {
2484
+ // a disconnect set the flag and took the lock while this call was queued behind it.
2485
+ // Creating a context now would leave one open on a room nothing tears down again
2486
+ return;
2487
+ }
2488
+ this.audioContextReleased = false;
2489
+ const providedAudioContext =
2490
+ typeof this.options.webAudioMix !== 'boolean'
2491
+ ? this.options.webAudioMix.audioContext
2492
+ : undefined;
2493
+ if (providedAudioContext) {
2494
+ // override audio context with custom audio context if supplied by user
2495
+ await this.setRoomAudioContext(providedAudioContext, false);
2496
+ } else if (!this.audioContext || this.audioContext.state === 'closed') {
2497
+ // by using an AudioContext, it reduces lag on audio elements
2498
+ // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
2499
+ await this.setRoomAudioContext(getNewAudioContext() ?? undefined, true);
2500
+ }
2352
2501
 
2353
- if (this.options.webAudioMix) {
2354
- this.remoteParticipants.forEach((participant) =>
2355
- participant.setAudioContext(this.audioContext),
2356
- );
2502
+ if (this.audioContext && this.audioContext.state === 'suspended') {
2503
+ // for iOS a newly created AudioContext is always in `suspended` state.
2504
+ // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
2505
+ try {
2506
+ await Promise.race([this.audioContext.resume(), sleep(200)]);
2507
+ } catch (e: any) {
2508
+ this.log.warn('Could not resume audio context', { error: e });
2509
+ }
2510
+ }
2511
+
2512
+ const newContextIsRunning = this.audioContext?.state === 'running';
2513
+ if (newContextIsRunning !== this.canPlaybackAudio) {
2514
+ this.audioEnabled = newContextIsRunning;
2515
+ this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
2516
+ }
2517
+ } finally {
2518
+ unlock();
2357
2519
  }
2520
+ }
2358
2521
 
2359
- this.localParticipant.setAudioContext(this.audioContext);
2522
+ /**
2523
+ * Points the room and its participants at `audioContext`, rebuilding the web audio graph of
2524
+ * every audio track in it. Closes the previous context if the SDK `owned` it.
2525
+ */
2526
+ private async setRoomAudioContext(audioContext: AudioContext | undefined, owned: boolean) {
2527
+ const ownsAudioContext = !!audioContext && owned;
2528
+ if (this.audioContext === audioContext) {
2529
+ this.ownsAudioContext = ownsAudioContext;
2530
+ return;
2531
+ }
2532
+ const contextToClose = this.ownsAudioContext ? this.audioContext : undefined;
2533
+ this.audioContext = audioContext;
2534
+ this.ownsAudioContext = ownsAudioContext;
2360
2535
 
2361
- if (this.audioContext && this.audioContext.state === 'suspended') {
2362
- // for iOS a newly created AudioContext is always in `suspended` state.
2363
- // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
2364
- try {
2365
- await Promise.race([this.audioContext.resume(), sleep(200)]);
2366
- } catch (e: any) {
2367
- this.log.warn('Could not resume audio context', { error: e });
2536
+ try {
2537
+ // every audio track rebuilds its nodes in the new context, which has to happen before the
2538
+ // previous one is closed so that processors can move off it while it is still alive
2539
+ if (this.options.webAudioMix) {
2540
+ await Promise.all(
2541
+ Array.from(this.remoteParticipants.values()).map((participant) =>
2542
+ participant.setAudioContext(audioContext),
2543
+ ),
2544
+ );
2545
+ }
2546
+ await this.localParticipant.setAudioContext(audioContext);
2547
+ } catch (error) {
2548
+ // the context is being abandoned either way, so a track that failed to move off it must
2549
+ // not stop us closing it — that would leak the context this whole path exists to release
2550
+ this.log.warn('Could not move audio tracks to the new audio context', {
2551
+ ...this.logContext,
2552
+ error,
2553
+ });
2554
+ } finally {
2555
+ if (contextToClose && contextToClose.state !== 'closed') {
2556
+ try {
2557
+ await contextToClose.close();
2558
+ } catch (error) {
2559
+ this.log.warn('Could not close audio context', { ...this.logContext, error });
2560
+ }
2368
2561
  }
2369
2562
  }
2563
+ }
2370
2564
 
2371
- const newContextIsRunning = this.audioContext?.state === 'running';
2372
- if (newContextIsRunning !== this.canPlaybackAudio) {
2373
- this.audioEnabled = newContextIsRunning;
2374
- this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
2565
+ /**
2566
+ * Detaches the web audio graph the SDK built from the current audio context and closes that
2567
+ * context if the SDK created it. A context provided through `webAudioMix` is left untouched.
2568
+ */
2569
+ private async releaseAudioContext(retainedAudioTracks: LocalAudioTrack[] = []) {
2570
+ // both reads happen before the first await, so they describe the room as it was when the
2571
+ // disconnect started rather than after a reconnect has had a chance to run
2572
+ this.audioContextReleased = true;
2573
+ const contextToRelease = this.audioContext;
2574
+ const unlock = await this.audioContextMutex.lock();
2575
+ try {
2576
+ if (!this.audioContextReleased || this.audioContext !== contextToRelease) {
2577
+ // a reconnect acquired a new context while we were waiting, closing it would leave the
2578
+ // reconnected room without one
2579
+ return;
2580
+ }
2581
+ if (!this.ownsAudioContext) {
2582
+ // a provided context stays open, so tracks that outlive the room stay functional on it.
2583
+ // The tracks this room detached have released their nodes during track teardown already.
2584
+ // read under the lock: an acquire queued behind an in-flight release flips this flag
2585
+ return;
2586
+ }
2587
+ try {
2588
+ // tracks that outlive the room aren't reachable through the local participant anymore,
2589
+ // but still hold web audio nodes in the context we're about to close
2590
+ await Promise.all(retainedAudioTracks.map((track) => track.setAudioContext(undefined)));
2591
+ } catch (error) {
2592
+ this.log.warn('Could not detach retained audio tracks from the audio context', {
2593
+ ...this.logContext,
2594
+ error,
2595
+ });
2596
+ }
2597
+ // the participant maps have already been cleared by `handleDisconnect`, so this reaches no
2598
+ // tracks of its own — `retainedAudioTracks` above is what detaches them
2599
+ await this.setRoomAudioContext(undefined, false);
2600
+ } finally {
2601
+ unlock();
2375
2602
  }
2376
2603
  }
2377
2604
 
@@ -2402,7 +2629,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2402
2629
  );
2403
2630
  }
2404
2631
  if (this.options.webAudioMix) {
2405
- participant.setAudioContext(this.audioContext);
2632
+ participant
2633
+ .setAudioContext(this.audioContext)
2634
+ .catch((error) =>
2635
+ this.log.warn('Could not set audio context', { ...this.logContext, error }),
2636
+ );
2406
2637
  }
2407
2638
  if (this.options.audioOutput?.deviceId) {
2408
2639
  participant
@@ -2566,7 +2797,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2566
2797
  }
2567
2798
  // Advertise deflate-raw decompression support so peers know they can send us compressed data
2568
2799
  // streams (gated separately from clientProtocol — see the data streams v2 spec).
2569
- if (isCompressionStreamSupported()) {
2800
+ if (isDeflateRawCompressionSupported()) {
2570
2801
  capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
2571
2802
  }
2572
2803
  return capabilities;
@@ -2903,7 +3134,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2903
3134
  new LocalAudioTrack(
2904
3135
  publishOptions.useRealTracks && navigator.mediaDevices?.getUserMedia
2905
3136
  ? (await navigator.mediaDevices.getUserMedia({ audio: true })).getAudioTracks()[0]
2906
- : getEmptyAudioStreamTrack(),
3137
+ : this.acquireEmptyAudioStreamTrack(),
2907
3138
  undefined,
2908
3139
  false,
2909
3140
  this.audioContext,
@@ -2946,7 +3177,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2946
3177
  info.tracks = [...info.tracks, videoTrack];
2947
3178
  }
2948
3179
  if (participantOptions.audio) {
2949
- const dummyTrack = getEmptyAudioStreamTrack();
3180
+ const dummyTrack = this.acquireEmptyAudioStreamTrack();
2950
3181
  const audioTrack = new TrackInfo({
2951
3182
  source: TrackSource.MICROPHONE,
2952
3183
  sid: Math.floor(Math.random() * 10_000).toString(),
@@ -3060,6 +3291,7 @@ export type RoomEventCallbacks = {
3060
3291
  kind?: DataPacket_Kind,
3061
3292
  topic?: string,
3062
3293
  encryptionType?: Encryption_Type,
3294
+ participantIdentity?: string,
3063
3295
  ) => void;
3064
3296
  sipDTMFReceived: (dtmf: SipDTMF, participant?: RemoteParticipant) => void;
3065
3297
  transcriptionReceived: (
@@ -17,15 +17,22 @@ class FakeDataChannel extends EventTarget {
17
17
 
18
18
  onbufferedamountlow: (() => void) | null = null;
19
19
 
20
+ readyState: RTCDataChannelState = 'open';
21
+
20
22
  send = vi.fn();
21
23
 
22
- close = vi.fn();
24
+ close = vi.fn(() => {
25
+ this.readyState = 'closing';
26
+ });
23
27
 
24
28
  constructor(public label: string) {
25
29
  super();
26
30
  }
27
31
  }
28
32
 
33
+ const reliableHandle = (manager: DataChannelManager) =>
34
+ manager.getHandle(DataChannelKind.RELIABLE) as RTCDataChannel;
35
+
29
36
  const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
30
37
 
31
38
  function makeManager(overrides?: Partial<DataChannelManagerOptions>) {
@@ -89,22 +96,32 @@ describe('DataChannelManager', () => {
89
96
  manager.lossy.stopThresholdTuning();
90
97
  });
91
98
 
92
- it('recreating channels rejects waiters parked on the replaced handles', async () => {
93
- const { manager, pcManager, created } = makeManager();
99
+ it('recreating channels closes the replaced handles and rejects waiters parked on them', async () => {
100
+ const { manager, opts, pcManager, created } = makeManager();
94
101
  manager.createPublisherChannels(pcManager);
102
+ const firstGeneration = { ...created };
95
103
 
96
104
  // Park a reliable sender on the first-generation channel.
97
105
  created._reliable.bufferedAmount = 2 * 1024 * 1024;
98
- const parked = manager.reliable.waitForHeadroomWithLock();
106
+ const parked = manager.reliable.waitForHeadroomWithLock(reliableHandle(manager));
99
107
  parked.catch(() => {});
100
108
  await tick();
101
109
 
102
- // Safari null-id path: channels recreated without closing the old ones.
110
+ // Safari null-id path: channels recreated while the old ones are still open.
103
111
  manager.createPublisherChannels(pcManager);
104
112
 
105
113
  await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
114
+ for (const dc of Object.values(firstGeneration)) {
115
+ expect(dc.close).toHaveBeenCalled();
116
+ // Unhooked before closing, so the deliberate close isn't reported as an unexpected one.
117
+ expect(dc.onclose).toBeNull();
118
+ expect(dc.onbufferedamountlow).toBeNull();
119
+ }
120
+ expect(opts.onChannelClose).not.toHaveBeenCalled();
106
121
  // The gate recovers against the fresh (empty) channel.
107
- await expect(manager.reliable.waitForHeadroomWithLock()).resolves.toBeUndefined();
122
+ await expect(
123
+ manager.reliable.waitForHeadroomWithLock(reliableHandle(manager)),
124
+ ).resolves.toBeUndefined();
108
125
 
109
126
  manager.lossy.stopThresholdTuning();
110
127
  });
@@ -133,7 +150,7 @@ describe('DataChannelManager', () => {
133
150
  manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel);
134
151
 
135
152
  created._reliable.bufferedAmount = 2 * 1024 * 1024;
136
- const parked = manager.reliable.waitForHeadroomWithLock();
153
+ const parked = manager.reliable.waitForHeadroomWithLock(reliableHandle(manager));
137
154
  parked.catch(() => {});
138
155
  await tick();
139
156