livekit-client 2.22.2 → 2.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +35 -0
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +488 -438
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +954 -301
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +10 -2
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/api/SignalClient.d.ts +4 -2
  15. package/dist/src/api/SignalClient.d.ts.map +1 -1
  16. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  17. package/dist/src/api/utils.d.ts +1 -0
  18. package/dist/src/api/utils.d.ts.map +1 -1
  19. package/dist/src/e2ee/E2eeManager.d.ts +26 -0
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/types.d.ts +15 -1
  22. package/dist/src/e2ee/types.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  25. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
  27. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  28. package/dist/src/logger.d.ts +4 -0
  29. package/dist/src/logger.d.ts.map +1 -1
  30. package/dist/src/options.d.ts +14 -0
  31. package/dist/src/options.d.ts.map +1 -1
  32. package/dist/src/room/PCTransport.d.ts +78 -7
  33. package/dist/src/room/PCTransport.d.ts.map +1 -1
  34. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  35. package/dist/src/room/RTCEngine.d.ts +9 -0
  36. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  37. package/dist/src/room/Room.d.ts +1 -1
  38. package/dist/src/room/Room.d.ts.map +1 -1
  39. package/dist/src/room/defaults.d.ts.map +1 -1
  40. package/dist/src/room/errors.d.ts +18 -0
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/events.d.ts +3 -2
  43. package/dist/src/room/events.d.ts.map +1 -1
  44. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  45. package/dist/src/room/participant/publishUtils.d.ts +16 -0
  46. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  47. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  48. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  49. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  50. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  51. package/dist/src/room/track/options.d.ts +1 -1
  52. package/dist/src/room/track/utils.d.ts +13 -0
  53. package/dist/src/room/track/utils.d.ts.map +1 -1
  54. package/dist/src/room/utils.d.ts +66 -0
  55. package/dist/src/room/utils.d.ts.map +1 -1
  56. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  57. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  58. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  59. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  60. package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
  61. package/dist/ts4.2/api/utils.d.ts +9 -8
  62. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  63. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  64. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  65. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  66. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  67. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  68. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  69. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  70. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  71. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -85
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  74. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  75. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  76. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  77. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  78. package/dist/ts4.2/e2ee/types.d.ts +231 -217
  79. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  80. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  81. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -226
  83. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  84. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  85. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
  86. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  87. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  88. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  89. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  90. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  91. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  92. package/dist/ts4.2/index.d.ts +76 -76
  93. package/dist/ts4.2/logger.d.ts +66 -62
  94. package/dist/ts4.2/options.d.ts +163 -149
  95. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  96. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  97. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  98. package/dist/ts4.2/room/PCTransport.d.ts +246 -175
  99. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  100. package/dist/ts4.2/room/RTCEngine.d.ts +269 -261
  101. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  102. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  103. package/dist/ts4.2/room/Room.d.ts +354 -365
  104. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  105. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
  106. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
  107. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  108. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  109. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  110. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  111. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  112. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  113. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  114. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  115. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  116. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  117. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  118. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  119. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  120. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  121. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  122. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  123. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  125. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  126. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  127. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  128. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  129. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  130. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  131. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  132. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  133. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  134. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  135. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  136. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  137. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  138. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  139. package/dist/ts4.2/room/debounce.d.ts +11 -11
  140. package/dist/ts4.2/room/defaults.d.ts +9 -9
  141. package/dist/ts4.2/room/errors.d.ts +180 -162
  142. package/dist/ts4.2/room/events.d.ts +638 -637
  143. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
  144. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  145. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  146. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  147. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -37
  148. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  149. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  150. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  151. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  152. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  153. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  154. package/dist/ts4.2/room/stats.d.ts +71 -71
  155. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  156. package/dist/ts4.2/room/timers.d.ts +12 -12
  157. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  158. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  159. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  160. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  161. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
  162. package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
  163. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  164. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -93
  165. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
  166. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  167. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  168. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  169. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  170. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  171. package/dist/ts4.2/room/track/create.d.ts +25 -25
  172. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  173. package/dist/ts4.2/room/track/options.d.ts +403 -403
  174. package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
  175. package/dist/ts4.2/room/track/record.d.ts +13 -13
  176. package/dist/ts4.2/room/track/types.d.ts +28 -28
  177. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  178. package/dist/ts4.2/room/types.d.ts +127 -127
  179. package/dist/ts4.2/room/utils.d.ts +246 -180
  180. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  181. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  182. package/dist/ts4.2/test/mocks.d.ts +12 -12
  183. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  184. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  185. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  186. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  187. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  188. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  189. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  190. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  191. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  192. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  193. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  194. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  195. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  196. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  197. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  198. package/dist/ts4.2/version.d.ts +15 -15
  199. package/package.json +5 -4
  200. package/src/api/SignalClient.e2e.test.ts +1 -3
  201. package/src/api/SignalClient.test.ts +160 -10
  202. package/src/api/SignalClient.ts +75 -54
  203. package/src/api/WebSocketStream.ts +3 -8
  204. package/src/api/utils.ts +10 -0
  205. package/src/e2ee/E2eeManager.test.ts +196 -0
  206. package/src/e2ee/E2eeManager.ts +114 -19
  207. package/src/e2ee/types.ts +19 -1
  208. package/src/e2ee/worker/DataCryptor.ts +2 -1
  209. package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
  210. package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
  211. package/src/e2ee/worker/FrameCryptor.ts +20 -70
  212. package/src/e2ee/worker/e2ee.worker.ts +34 -11
  213. package/src/logger.ts +22 -0
  214. package/src/options.ts +16 -0
  215. package/src/room/PCTransport.test.ts +183 -7
  216. package/src/room/PCTransport.ts +197 -36
  217. package/src/room/PCTransportManager.ts +3 -1
  218. package/src/room/RTCEngine.test.ts +84 -0
  219. package/src/room/RTCEngine.ts +52 -12
  220. package/src/room/Room.test.ts +32 -6
  221. package/src/room/Room.ts +27 -7
  222. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
  223. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
  224. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  225. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  226. package/src/room/defaults.ts +1 -0
  227. package/src/room/errors.test.ts +36 -0
  228. package/src/room/errors.ts +27 -0
  229. package/src/room/events.ts +2 -1
  230. package/src/room/participant/LocalParticipant.ts +30 -14
  231. package/src/room/participant/publishUtils.test.ts +133 -0
  232. package/src/room/participant/publishUtils.ts +54 -38
  233. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  234. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  235. package/src/room/track/LocalTrack.ts +18 -7
  236. package/src/room/track/LocalVideoTrack.ts +25 -8
  237. package/src/room/track/RemoteAudioTrack.ts +3 -3
  238. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  239. package/src/room/track/RemoteVideoTrack.ts +29 -3
  240. package/src/room/track/options.ts +1 -1
  241. package/src/room/track/utils.test.ts +201 -2
  242. package/src/room/track/utils.ts +88 -0
  243. package/src/room/utils.test.ts +211 -0
  244. package/src/room/utils.ts +129 -0
  245. package/src/test/compressionStreamStubs.ts +53 -0
@@ -34,51 +34,130 @@ const startBitrateMultiplier = 0.9;
34
34
  /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
35
35
  const maxStartBitrateKbps = 1000;
36
36
 
37
+ /**
38
+ * Minimum target bitrate in kbps for the start bitrate hint. Below this, seeding above the
39
+ * real capacity costs more than the ramp it saves, so libwebrtc's default is left in place.
40
+ */
41
+ const minTargetBitrateKbps = 300;
42
+
37
43
  const debounceInterval = 20;
38
44
 
39
45
  /**
40
- * Applies the configured start bitrate when this media section belongs to `cid`.
41
- * This SDP munging is used for a bitrate setting that cannot be applied through
42
- * `RTCRtpEncodingParameters`.
46
+ * Codec payload for `codec` in this media section, when the section carries `cid`.
43
47
  *
44
- * Returns `undefined` when the section does not belong to the track, `0` when
45
- * it does but does not offer the requested codec, and the codec payload when the
46
- * requested codec is present (whether the bitrate was added or already set).
48
+ * Returns `undefined` when the section does not belong to the track, `0` when it does but
49
+ * does not offer the requested codec, and the codec payload otherwise.
47
50
  *
48
51
  * @internal
49
52
  */
50
- export function applyVideoStartBitrate(
53
+ export function findTrackCodecPayload(
51
54
  media: MediaDescription,
52
55
  cid: string,
53
56
  codec: string,
54
- maxbr: number,
55
- isScreenShare = false,
56
57
  ): number | undefined {
57
58
  if (!media.msid?.includes(cid)) {
58
59
  return undefined;
59
60
  }
61
+ return media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
62
+ }
60
63
 
61
- const codecPayload =
62
- media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
63
- if (codecPayload === 0) {
64
- return 0;
64
+ /**
65
+ * Start bitrate hinted for a single track, or `undefined` when its target is too low to
66
+ * be worth seeding.
67
+ *
68
+ * 90% of the target leaves ~10% headroom for the estimator to settle. The same multiplier
69
+ * is used for every codec because the target already reflects the codec's efficiency.
70
+ * Camera is capped at 1 Mbps so the estimator does not open too aggressively on a
71
+ * high-bitrate track; screen share is exempt, because its content needs the bitrate
72
+ * immediately to stay legible.
73
+ *
74
+ * TODO: adjust dynamically from network conditions (e.g. a previous BWE estimate) rather
75
+ * than a fixed cap.
76
+ *
77
+ * @internal
78
+ */
79
+ export function computeTrackStartBitrate(trackbr: TrackBitrateInfo): number | undefined {
80
+ if (trackbr.maxbr < minTargetBitrateKbps) {
81
+ return undefined;
65
82
  }
83
+ const calculated = Math.round(trackbr.maxbr * startBitrateMultiplier);
84
+ return trackbr.isScreenShare ? calculated : Math.min(calculated, maxStartBitrateKbps);
85
+ }
66
86
 
67
- // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
68
- // from starting too aggressively. Screen share is not capped since text/UI
69
- // clarity requires high bitrate from the start.
70
- // TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
71
- const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
72
- const startBitrate = isScreenShare
73
- ? calculatedStartBitrate
74
- : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
87
+ /**
88
+ * The single start bitrate for this peer connection: the largest hint among the video
89
+ * m-sections of `media` that map to a published track.
90
+ *
91
+ * libwebrtc reads `x-google-start-bitrate` per m-section but applies it to the shared
92
+ * `Call` (`WebRtcVideoSendChannel::ApplyChangedParams` -> `SetSdpBitrateParameters`), where
93
+ * `RtpBitrateConfigurator` holds one config for the whole connection. Differing per-section
94
+ * values are therefore last-writer-wins, decided by m-section order, so every video section
95
+ * gets the same number instead.
96
+ *
97
+ * Only sections that can send local media are considered. `trackBitrates` is append-only and an
98
+ * unpublished section keeps its `a=msid`, so matching on msid alone would still pair a stale
99
+ * entry with its old section — letting an uncapped screen-share target seed a connection that
100
+ * now carries only a camera, or consuming the one-shot hint on a section that sends nothing.
101
+ *
102
+ * The direction is the discriminator, as an exclusion rather than a match: `recvonly` and
103
+ * `inactive` are the only directions that cannot carry local media, and they are exactly the
104
+ * two a dead section lands on — unpublish sets `inactive` (explicitly in
105
+ * `LocalParticipant.unpublishTrack`, and via `removeTrack`'s sendonly -> inactive transition for
106
+ * the simulcast senders), `removeTrack` on a `sendrecv` transceiver leaves `recvonly`, and the
107
+ * pre-populated placeholders are `recvonly`. Everything else sends: `sendonly` from the
108
+ * `addTransceiver` path, `sendrecv` from the legacy `addTrack` fallback (which reuses a
109
+ * transceiver rather than creating a sendonly one), and a section with no direction attribute,
110
+ * which SDP defaults to `sendrecv`.
111
+ *
112
+ * @internal
113
+ */
114
+ export function computeConnectionStartBitrate(
115
+ media: MediaDescription[],
116
+ trackBitrates: TrackBitrateInfo[],
117
+ ): number | undefined {
118
+ let connectionStartBitrate: number | undefined;
119
+ for (const m of media) {
120
+ if (m.type !== 'video' || m.direction === 'recvonly' || m.direction === 'inactive') {
121
+ continue;
122
+ }
123
+ for (const trackbr of trackBitrates) {
124
+ if (!trackbr.cid) {
125
+ continue;
126
+ }
127
+ const codecPayload = findTrackCodecPayload(m, trackbr.cid, trackbr.codec);
128
+ if (codecPayload === undefined) {
129
+ continue;
130
+ }
131
+ const startBitrate = codecPayload > 0 ? computeTrackStartBitrate(trackbr) : undefined;
132
+ if (
133
+ startBitrate !== undefined &&
134
+ (connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)
135
+ ) {
136
+ connectionStartBitrate = startBitrate;
137
+ }
138
+ break;
139
+ }
140
+ }
141
+ return connectionStartBitrate;
142
+ }
75
143
 
144
+ /**
145
+ * Declares `x-google-start-bitrate` on `codecPayload`'s fmtp. This SDP munging is used for
146
+ * a bitrate setting that cannot be applied through `RTCRtpEncodingParameters`.
147
+ *
148
+ * Returns whether the section now carries the hint.
149
+ *
150
+ * @internal
151
+ */
152
+ export function applyVideoStartBitrate(
153
+ media: MediaDescription,
154
+ codecPayload: number,
155
+ startBitrate: number,
156
+ ): boolean {
76
157
  const fmtp = media.fmtp.find((entry) => entry.payload === codecPayload);
77
158
  if (fmtp) {
78
- // If another track's fmtp already has a start bitrate, it cannot be
79
- // overridden here because the payload type is shared across the bundle.
80
- // This forces every track sharing that payload to use the initial track's
81
- // start bitrate.
159
+ // A payload type is shared across the bundle, so a value written for one section is
160
+ // already the connection-level one; leave it rather than rewrite it.
82
161
  if (!fmtp.config.includes('x-google-start-bitrate')) {
83
162
  fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
84
163
  }
@@ -90,7 +169,7 @@ export function applyVideoStartBitrate(
90
169
  });
91
170
  }
92
171
 
93
- return codecPayload;
172
+ return true;
94
173
  }
95
174
 
96
175
  export const PCEvents = {
@@ -127,6 +206,11 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
127
206
 
128
207
  latestOfferId: number = 0;
129
208
 
209
+ private answeredOfferId: number = 0;
210
+
211
+ /** offerId (0 if unnumbered) of the answer being applied right now */
212
+ private answerInFlight: number | undefined;
213
+
130
214
  latestAcknowledgedOfferId: number = 0;
131
215
 
132
216
  private offerLock: Mutex;
@@ -141,6 +225,15 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
141
225
 
142
226
  trackBitrates: TrackBitrateInfo[] = [];
143
227
 
228
+ /**
229
+ * Whether an offer carrying the connection-level `x-google-start-bitrate` has been
230
+ * accepted locally. The hint is written once per peer connection: libwebrtc retains
231
+ * `start_bitrate_bps` in `RtpBitrateConfigurator` and re-applies it on network route
232
+ * changes, so a later rewrite is at best a no-op and at worst restarts a converged
233
+ * bandwidth estimator. A new peer connection (full reconnect) seeds a new estimator.
234
+ */
235
+ private hasAppliedVideoStartBitrate = false;
236
+
144
237
  remoteStereoMids: string[] = [];
145
238
 
146
239
  remoteNackMids: string[] = [];
@@ -250,6 +343,36 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
250
343
  });
251
344
  return false;
252
345
  }
346
+ if (sd.type === 'answer') {
347
+ if (
348
+ isDuplicateAnswer(
349
+ offerId,
350
+ this.answerInFlight,
351
+ this.answeredOfferId,
352
+ this._pc?.signalingState,
353
+ this.pendingInitialOffer !== undefined,
354
+ )
355
+ ) {
356
+ // a repeat of an answer already applied or being applied (e.g.
357
+ // duplicated by a proxy): there is no local offer left to answer
358
+ this.log.warn('ignoring duplicate answer', { offerId });
359
+ return false;
360
+ }
361
+ // claimed before any await, so a copy arriving mid-apply is caught too;
362
+ // released however the apply ends, so a genuine retry is still possible
363
+ this.answerInFlight = offerId;
364
+ }
365
+ try {
366
+ return await this.applyRemoteDescription(sd, offerId);
367
+ } finally {
368
+ if (sd.type === 'answer') this.answerInFlight = undefined;
369
+ }
370
+ }
371
+
372
+ private async applyRemoteDescription(
373
+ sd: RTCSessionDescriptionInit,
374
+ offerId: number,
375
+ ): Promise<boolean> {
253
376
  let mungedSDP: string | undefined = undefined;
254
377
  if (sd.type === 'offer') {
255
378
  let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
@@ -331,6 +454,8 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
331
454
  mungedSDP = write(sdpParsed);
332
455
  }
333
456
  await this.setMungedSDP(sd, mungedSDP, true);
457
+ // only a successfully applied answer makes later copies duplicates
458
+ if (sd.type === 'answer' && offerId > 0) this.answeredOfferId = offerId;
334
459
 
335
460
  if (this.pendingCandidates.length > 0) {
336
461
  this.iceLog.debug('flushing queued ICE candidates', {
@@ -453,6 +578,14 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
453
578
  this.log.debug('original offer', { sdp: offer.sdp });
454
579
 
455
580
  const sdpParsed = parse(offer.sdp ?? '');
581
+ // One value for every video m-section, written only on the first offer that carries
582
+ // local video: the hint is connection-level in libwebrtc, so differing per-section
583
+ // values would be last-writer-wins on m-section order. Offers before any video is
584
+ // published (data channel or audio only) find no target and leave the latch unset.
585
+ const connectionStartBitrate = this.hasAppliedVideoStartBitrate
586
+ ? undefined
587
+ : computeConnectionStartBitrate(sdpParsed.media, this.trackBitrates);
588
+ let appliedVideoStartBitrate = false;
456
589
  sdpParsed.media.forEach((media) => {
457
590
  ensureIPAddrMatchVersion(media);
458
591
  if (media.type === 'audio') {
@@ -463,19 +596,21 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
463
596
  return false;
464
597
  }
465
598
 
466
- const codecPayload = applyVideoStartBitrate(
467
- media,
468
- trackbr.cid,
469
- trackbr.codec,
470
- trackbr.maxbr,
471
- trackbr.isScreenShare,
472
- );
599
+ const codecPayload = findTrackCodecPayload(media, trackbr.cid, trackbr.codec);
473
600
  if (codecPayload === undefined) {
474
601
  return false;
475
602
  }
476
603
 
477
- if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
478
- this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
604
+ if (codecPayload > 0) {
605
+ if (connectionStartBitrate !== undefined) {
606
+ appliedVideoStartBitrate =
607
+ applyVideoStartBitrate(media, codecPayload, connectionStartBitrate) ||
608
+ appliedVideoStartBitrate;
609
+ }
610
+
611
+ if (isSVCCodec(trackbr.codec) && !isSafari()) {
612
+ this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
613
+ }
479
614
  }
480
615
 
481
616
  return true;
@@ -500,7 +635,13 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
500
635
  });
501
636
  return;
502
637
  }
503
- await this.setMungedSDP(offer, write(sdpParsed));
638
+ const mungedSdp = write(sdpParsed);
639
+ await this.setMungedSDP(offer, mungedSdp);
640
+ // setMungedSDP falls back to the unmunged SDP on rejection. Only consume the
641
+ // one-shot hint once the SDP carrying it has been accepted locally.
642
+ if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
643
+ this.hasAppliedVideoStartBitrate = true;
644
+ }
504
645
  this.onOffer(offer, this.latestOfferId);
505
646
  } finally {
506
647
  unlock();
@@ -952,6 +1093,26 @@ export function conformBundledCodecFmtp(
952
1093
  }
953
1094
 
954
1095
  /** @internal */
1096
+ /**
1097
+ * Whether an incoming answer repeats one already applied or being applied.
1098
+ * Any answer for the offer whose answer is in flight is a duplicate, numbered
1099
+ * or not. With an offerId (current servers) an answer is also a duplicate if
1100
+ * that offer's answer was already applied. Without one, fall back to the
1101
+ * signaling state: in `stable` no local offer is pending, unless the initial
1102
+ * offer is still deferred (it is set locally only once its answer arrives).
1103
+ */
1104
+ export function isDuplicateAnswer(
1105
+ offerId: number,
1106
+ answerInFlight: number | undefined,
1107
+ answeredOfferId: number,
1108
+ signalingState: RTCSignalingState | undefined,
1109
+ hasPendingInitialOffer: boolean,
1110
+ ): boolean {
1111
+ if (answerInFlight === offerId) return true;
1112
+ if (offerId > 0) return offerId === answeredOfferId;
1113
+ return signalingState === 'stable' && !hasPendingInitialOffer;
1114
+ }
1115
+
955
1116
  export function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
956
1117
  stereoMids: string[];
957
1118
  nackMids: string[];
@@ -229,7 +229,9 @@ export class PCTransportManager {
229
229
  this.publisher.getConnectionState() !== 'connecting'
230
230
  ) {
231
231
  this.log.debug('negotiation required, start negotiating');
232
- this.publisher.negotiate();
232
+ this.publisher.negotiate().catch((err) => {
233
+ this.log.error(err);
234
+ });
233
235
  }
234
236
  await Promise.all(
235
237
  this.requiredTransports?.map((transport) =>
@@ -10,6 +10,7 @@ import { PCTransportState } from './PCTransportManager';
10
10
  import RTCEngine, { DataChannelKind } from './RTCEngine';
11
11
  import { roomOptionDefaults } from './defaults';
12
12
  import { PublishDataError, UnexpectedConnectionState } from './errors';
13
+ import { EngineEvent } from './events';
13
14
 
14
15
  describe('RTCEngine', () => {
15
16
  const originalRTCRtpSender = window.RTCRtpSender;
@@ -919,4 +920,87 @@ describe('RTCEngine', () => {
919
920
  expect(handleDisconnect).not.toHaveBeenCalled();
920
921
  });
921
922
  });
923
+
924
+ describe('negotiate', () => {
925
+ /**
926
+ * An engine whose `pcManager` parks in `negotiate()` until its abort controller fires, so
927
+ * several negotiations can be held in flight at once. The controllers are captured so tests
928
+ * can check the abort actually reached each in-flight call.
929
+ */
930
+ function stubNegotiableEngine(engine: RTCEngine) {
931
+ const controllers: AbortController[] = [];
932
+ const negotiate = vi.fn((abortController: AbortController) => {
933
+ controllers.push(abortController);
934
+ return new Promise<void>((_resolve, reject) => {
935
+ abortController.signal.addEventListener(
936
+ 'abort',
937
+ () => reject(new Error('negotiation aborted')),
938
+ { once: true },
939
+ );
940
+ });
941
+ });
942
+ Object.assign(engine as unknown as Record<string, unknown>, {
943
+ _isClosed: false,
944
+ pcManager: {
945
+ requirePublisher: vi.fn(),
946
+ // a non-empty transceiver list keeps negotiate() off the createDataChannels path
947
+ publisher: { getTransceivers: () => [{}], off: vi.fn(), once: vi.fn() },
948
+ negotiate,
949
+ },
950
+ handleDisconnect: vi.fn(),
951
+ });
952
+ return { negotiate, controllers };
953
+ }
954
+
955
+ const pendingAborts = (engine: RTCEngine) =>
956
+ (engine as unknown as { pendingNegotiationAborts: Set<() => void> }).pendingNegotiationAborts;
957
+
958
+ it('does not add a Closing/Restarting listener per in-flight negotiate call', async () => {
959
+ const engine = new RTCEngine(roomOptionDefaults);
960
+ const { negotiate, controllers } = stubNegotiableEngine(engine);
961
+
962
+ // A burst of server-initiated renegotiations used to stack a listener pair per call, which
963
+ // trips the emitter's max-listener warning at 11.
964
+ const closingBaseline = engine.listenerCount(EngineEvent.Closing);
965
+ const restartingBaseline = engine.listenerCount(EngineEvent.Restarting);
966
+
967
+ const pending = Array.from({ length: 20 }, () => engine.negotiate());
968
+ await tick();
969
+
970
+ expect(negotiate).toHaveBeenCalledTimes(20);
971
+ expect(engine.listenerCount(EngineEvent.Closing)).toBe(closingBaseline);
972
+ expect(engine.listenerCount(EngineEvent.Restarting)).toBe(restartingBaseline);
973
+
974
+ // The single central listener still has to reach every one of them.
975
+ engine.emit(EngineEvent.Closing);
976
+ await expect(Promise.all(pending)).resolves.toHaveLength(20);
977
+ expect(controllers).toHaveLength(20);
978
+ expect(controllers.every((c) => c.signal.aborted)).toBe(true);
979
+ expect(pendingAborts(engine).size).toBe(0);
980
+ });
981
+
982
+ it('aborts in-flight negotiations on restart without disarming later ones', async () => {
983
+ const engine = new RTCEngine(roomOptionDefaults);
984
+ const { controllers } = stubNegotiableEngine(engine);
985
+
986
+ const firstBatch = [engine.negotiate(), engine.negotiate()];
987
+ await tick();
988
+ engine.emit(EngineEvent.Restarting);
989
+ await expect(Promise.all(firstBatch)).resolves.toHaveLength(2);
990
+ expect(controllers.every((c) => c.signal.aborted)).toBe(true);
991
+
992
+ // Restarting fires on every reconnect attempt, so the fan-out point has to re-arm: a
993
+ // negotiation started after a restart must not be aborted on arrival.
994
+ const afterRestart = engine.negotiate();
995
+ await tick();
996
+ expect(controllers).toHaveLength(3);
997
+ expect(controllers[2].signal.aborted).toBe(false);
998
+ expect(pendingAborts(engine).size).toBe(1);
999
+
1000
+ engine.emit(EngineEvent.Restarting);
1001
+ await expect(afterRestart).resolves.toBeUndefined();
1002
+ expect(controllers[2].signal.aborted).toBe(true);
1003
+ expect(pendingAborts(engine).size).toBe(0);
1004
+ });
1005
+ });
922
1006
  });
@@ -173,6 +173,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
173
173
  return !!this.reconnectTimeout;
174
174
  }
175
175
 
176
+ get serverVersion(): string | undefined {
177
+ return (
178
+ this.latestJoinResponse?.serverInfo?.version ||
179
+ this.latestJoinResponse?.serverVersion ||
180
+ undefined
181
+ );
182
+ }
183
+
176
184
  /**
177
185
  * Owns the data channels: the three flow-controlled publisher wrappers (engine-lifetime; the
178
186
  * RTCDataChannel handles underneath are attached/detached as peer connections come and go, with
@@ -260,6 +268,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
260
268
  /** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
261
269
  private transportConnectingSince?: number;
262
270
 
271
+ /**
272
+ * Abort handlers for the in-flight `negotiate()` calls, which a single central
273
+ * Closing/Restarting listener pair fans out to. Registering a listener pair per call instead
274
+ * accumulates them on the engine emitter and trips its max-listener warning under
275
+ * renegotiation bursts.
276
+ */
277
+ private pendingNegotiationAborts = new Set<() => void>();
278
+
263
279
  constructor(private options: InternalRoomOptions) {
264
280
  super();
265
281
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
@@ -300,6 +316,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
300
316
  this.client.onParticipantUpdate = (updates) =>
301
317
  this.emit(EngineEvent.ParticipantUpdate, updates);
302
318
  this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
319
+
320
+ const abortPendingNegotiations = () => {
321
+ // Iterate a copy: each handler removes itself from the set as its negotiation settles.
322
+ for (const abort of Array.from(this.pendingNegotiationAborts)) {
323
+ abort();
324
+ }
325
+ };
326
+ this.on(EngineEvent.Closing, abortPendingNegotiations);
327
+ this.on(EngineEvent.Restarting, abortPendingNegotiations);
303
328
  }
304
329
 
305
330
  /** @internal */
@@ -655,8 +680,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
655
680
  sdp: sd.sdp,
656
681
  midToTrackId,
657
682
  });
658
- this.midToTrackId = midToTrackId;
683
+ // in dual PC mode the publisher answer carries no mapping (the server's publisher
684
+ // transport has no sending tracks) and must not clobber the subscriber offer mapping
685
+ if (this.pcManager.mode === 'publisher-only') {
686
+ this.midToTrackId = midToTrackId;
687
+ }
659
688
  await this.pcManager.setPublisherAnswer(sd, offerId);
689
+ this.emit(EngineEvent.RequestSubscribedCodecRefresh);
660
690
  };
661
691
 
662
692
  // add candidate on trickle
@@ -726,7 +756,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
726
756
 
727
757
  this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
728
758
  this.addMediaSections(requirement.numAudios, requirement.numVideos);
729
- this.negotiate();
759
+ this.negotiate().catch((err) => {
760
+ this.log.error(err);
761
+ });
730
762
  };
731
763
 
732
764
  this.client.onPublishDataTrackResponse = (event: PublishDataTrackResponse) => {
@@ -918,12 +950,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
918
950
  this.log.error('Received encrypted packet but E2EE not set up');
919
951
  return;
920
952
  }
921
- const decryptedData = await this.e2eeManager?.handleEncryptedData(
922
- dp.value.value.encryptedValue as NonSharedUint8Array,
923
- dp.value.value.iv as NonSharedUint8Array,
924
- dp.participantIdentity,
925
- dp.value.value.keyIndex,
926
- );
953
+ let decryptedData;
954
+ try {
955
+ decryptedData = await this.e2eeManager.handleEncryptedData(
956
+ dp.value.value.encryptedValue as NonSharedUint8Array,
957
+ dp.value.value.iv as NonSharedUint8Array,
958
+ dp.participantIdentity,
959
+ dp.value.value.keyIndex,
960
+ );
961
+ } catch (err) {
962
+ this.log.debug('failed to decrypt data packet', {
963
+ error: err,
964
+ participantIdentity: dp.participantIdentity,
965
+ });
966
+ return;
967
+ }
927
968
  const decryptedPacket = EncryptedPacketPayload.fromBinary(decryptedData.payload);
928
969
  const newDp = new DataPacket({
929
970
  value: decryptedPacket.value,
@@ -1786,8 +1827,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1786
1827
  if (this.isClosed) {
1787
1828
  reject(new NegotiationError('cannot negotiate on closed engine'));
1788
1829
  }
1789
- this.on(EngineEvent.Closing, handleClosed);
1790
- this.on(EngineEvent.Restarting, handleClosed);
1830
+ this.pendingNegotiationAborts.add(handleClosed);
1791
1831
  this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1792
1832
  this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1793
1833
 
@@ -1811,8 +1851,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1811
1851
  reject(new Error(String(e)));
1812
1852
  }
1813
1853
  } finally {
1814
- this.off(EngineEvent.Closing, handleClosed);
1815
- this.off(EngineEvent.Restarting, handleClosed);
1854
+ this.pendingNegotiationAborts.delete(handleClosed);
1816
1855
  }
1817
1856
  });
1818
1857
  }
@@ -2074,6 +2113,7 @@ export type EngineEventCallbacks = {
2074
2113
  joined: (joinResponse: JoinResponse) => void;
2075
2114
  tokenRefreshed: (token: string) => void;
2076
2115
  serverRegionsReported: (regions: RegionSettings) => void;
2116
+ requestSubscribedCodecRefresh: () => void;
2077
2117
  };
2078
2118
 
2079
2119
  export interface RegionStrategy {
@@ -10,6 +10,7 @@ import {
10
10
  } from '@livekit/protocol';
11
11
  import { afterEach, describe, expect, it, vi } from 'vitest';
12
12
  import MockMediaStreamTrack from '../test/MockMediaStreamTrack';
13
+ import { stubMissingDeflateRawSupport } from '../test/compressionStreamStubs';
13
14
  import Room, { ConnectionState } from './Room';
14
15
  import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
15
16
  import { EngineEvent, ParticipantEvent, RoomEvent } from './events';
@@ -46,8 +47,8 @@ describe('Active device switch', () => {
46
47
  });
47
48
 
48
49
  describe('Room signaling options', () => {
49
- it('advertises packet trailer capability when E2EE can handle trailers', async () => {
50
- const room = new Room();
50
+ /** Drives `connectSignal` with E2EE enabled against a stub engine, returning the `join` mock. */
51
+ async function connectSignalWithE2EE(room: Room) {
51
52
  const join = vi.fn().mockResolvedValue({
52
53
  joinResponse: new JoinResponse({
53
54
  room: { name: 'test-room', sid: 'room-sid' },
@@ -91,19 +92,44 @@ describe('Room signaling options', () => {
91
92
  new AbortController(),
92
93
  );
93
94
 
95
+ return join;
96
+ }
97
+
98
+ function expectCapabilities(
99
+ join: ReturnType<typeof vi.fn>,
100
+ clientInfoCapabilities: Array<ClientInfo_Capability>,
101
+ ) {
94
102
  expect(join).toHaveBeenCalledWith(
95
103
  'wss://test.livekit.io',
96
104
  'test-token',
97
105
  expect.objectContaining({
98
- clientInfoCapabilities: [
99
- ClientInfo_Capability.CAP_PACKET_TRAILER,
100
- ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
101
- ],
106
+ clientInfoCapabilities,
102
107
  e2eeEnabled: true,
103
108
  }),
104
109
  expect.any(AbortSignal),
105
110
  false,
106
111
  );
112
+ }
113
+
114
+ it('advertises packet trailer capability when E2EE can handle trailers', async () => {
115
+ const join = await connectSignalWithE2EE(new Room());
116
+
117
+ expectCapabilities(join, [
118
+ ClientInfo_Capability.CAP_PACKET_TRAILER,
119
+ ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
120
+ ]);
121
+ });
122
+
123
+ it('does not advertise deflate-raw compression when the runtime cannot decompress it', async () => {
124
+ const restoreCompressionStreams = stubMissingDeflateRawSupport();
125
+ try {
126
+ const join = await connectSignalWithE2EE(new Room());
127
+
128
+ // Otherwise peers would compress what they send us and we'd have to drop it.
129
+ expectCapabilities(join, [ClientInfo_Capability.CAP_PACKET_TRAILER]);
130
+ } finally {
131
+ restoreCompressionStreams();
132
+ }
107
133
  });
108
134
  });
109
135