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
@@ -9,6 +9,10 @@ import {
9
9
  Encryption_Type,
10
10
  } from '@livekit/protocol';
11
11
  import { describe, expect, it } from 'vitest';
12
+ import {
13
+ stubMissingCompressionStreams,
14
+ stubMissingDeflateRawSupport,
15
+ } from '../../../test/compressionStreamStubs';
12
16
  import { deflateRawCompress } from '../compression';
13
17
  import { STREAM_CHUNK_SIZE_BYTES } from '../constants';
14
18
  import IncomingDataStreamManager from './IncomingDataStreamManager';
@@ -675,6 +679,37 @@ describe('IncomingDataStreamManager', () => {
675
679
  'Participant alice unexpectedly disconnected in the middle of sending data',
676
680
  );
677
681
  });
682
+
683
+ it('should error in flight streams when the controllers are cleared on room disconnect', async () => {
684
+ const manager = new IncomingDataStreamManager();
685
+ manager.setConnected(true);
686
+
687
+ const textReaderPromise = new Promise<TextStreamReader>((resolve) => {
688
+ manager.registerTextStreamHandler('my-topic', (reader) => resolve(reader));
689
+ });
690
+ const byteReaderPromise = new Promise<ByteStreamReader>((resolve) => {
691
+ manager.registerByteStreamHandler('my-topic', (reader) => resolve(reader));
692
+ });
693
+
694
+ manager.handleDataStreamPacket(headerPacket('text-1', 'textHeader'), Encryption_Type.NONE);
695
+ manager.handleDataStreamPacket(
696
+ chunkPacket('text-1', 0, new Uint8Array([0x68, 0x69])),
697
+ Encryption_Type.NONE,
698
+ );
699
+ manager.handleDataStreamPacket(headerPacket('byte-1', 'byteHeader'), Encryption_Type.NONE);
700
+ manager.handleDataStreamPacket(
701
+ chunkPacket('byte-1', 0, new Uint8Array([0x01])),
702
+ Encryption_Type.NONE,
703
+ );
704
+
705
+ // Room#handleDisconnect calls this while the streams are still open
706
+ manager.clearControllers();
707
+
708
+ const textReader = await textReaderPromise;
709
+ const byteReader = await byteReaderPromise;
710
+ await expect(textReader.readAll()).rejects.toThrow('Room disconnected');
711
+ await expect(byteReader.readAll()).rejects.toThrow('Room disconnected');
712
+ });
678
713
  });
679
714
 
680
715
  describe('Receiving v2 data streams', () => {
@@ -907,18 +942,15 @@ describe('IncomingDataStreamManager', () => {
907
942
  expect(await reader.readAll()).toStrictEqual(text);
908
943
  });
909
944
 
910
- it(`should ignore a v2 TEXT data stream with compression if DecompressionStream doesn't exist`, async () => {
945
+ it.each([
946
+ [`DecompressionStream doesn't exist`, stubMissingCompressionStreams],
947
+ [`deflate-raw isn't a supported format`, stubMissingDeflateRawSupport],
948
+ ])(`should ignore a v2 TEXT data stream with compression if %s`, async (_label, stub) => {
911
949
  const text = 'hello world';
912
950
  const compressed = await deflateRawCompress(new TextEncoder().encode(text));
913
951
 
914
- let originalCompressionStream: typeof CompressionStream,
915
- originalDecompressionStream: typeof DecompressionStream;
952
+ const restoreCompressionStreams = stub();
916
953
  try {
917
- originalCompressionStream = CompressionStream;
918
- (globalThis as any).CompressionStream = undefined;
919
- originalDecompressionStream = DecompressionStream;
920
- (globalThis as any).DecompressionStream = undefined;
921
-
922
954
  const manager = new IncomingDataStreamManager();
923
955
  manager.setConnected(true);
924
956
 
@@ -977,23 +1009,19 @@ describe('IncomingDataStreamManager', () => {
977
1009
  Promise.race([readerPromise, Promise.resolve('still pending')]),
978
1010
  ).resolves.toStrictEqual('still pending');
979
1011
  } finally {
980
- (globalThis as any).CompressionStream = originalCompressionStream!;
981
- (globalThis as any).DecompressionStream = originalDecompressionStream!;
1012
+ restoreCompressionStreams();
982
1013
  }
983
1014
  });
984
1015
 
985
- it(`should ignore a v2 BYTES data stream with compression if DecompressionStream doesn't exist`, async () => {
1016
+ it.each([
1017
+ [`DecompressionStream doesn't exist`, stubMissingCompressionStreams],
1018
+ [`deflate-raw isn't a supported format`, stubMissingDeflateRawSupport],
1019
+ ])(`should ignore a v2 BYTES data stream with compression if %s`, async (_label, stub) => {
986
1020
  const bytes = new Uint8Array([0x01, 0x02, 0x03]);
987
1021
  const compressed = await deflateRawCompress(bytes);
988
1022
 
989
- let originalCompressionStream: typeof CompressionStream,
990
- originalDecompressionStream: typeof DecompressionStream;
1023
+ const restoreCompressionStreams = stub();
991
1024
  try {
992
- originalCompressionStream = CompressionStream;
993
- (globalThis as any).CompressionStream = undefined;
994
- originalDecompressionStream = DecompressionStream;
995
- (globalThis as any).DecompressionStream = undefined;
996
-
997
1025
  const manager = new IncomingDataStreamManager();
998
1026
  manager.setConnected(true);
999
1027
 
@@ -1052,8 +1080,7 @@ describe('IncomingDataStreamManager', () => {
1052
1080
  Promise.race([readerPromise, Promise.resolve('still pending')]),
1053
1081
  ).resolves.toStrictEqual('still pending');
1054
1082
  } finally {
1055
- (globalThis as any).CompressionStream = originalCompressionStream!;
1056
- (globalThis as any).DecompressionStream = originalDecompressionStream!;
1083
+ restoreCompressionStreams();
1057
1084
  }
1058
1085
  });
1059
1086
 
@@ -10,7 +10,7 @@ import log from '../../../logger';
10
10
  import { type NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
11
11
  import { DataStreamError, DataStreamErrorReason } from '../../errors';
12
12
  import { type ByteStreamInfo, type StreamController, type TextStreamInfo } from '../../types';
13
- import { bigIntToNumber, isCompressionStreamSupported, numberToBigInt } from '../../utils';
13
+ import { bigIntToNumber, isDeflateRawCompressionSupported, numberToBigInt } from '../../utils';
14
14
  import { deflateRawDecompress, inflateRawTransform } from '../compression';
15
15
  import { DEFAULT_MAX_PAYLOAD_BYTE_LENGTH } from '../constants';
16
16
  import {
@@ -87,6 +87,17 @@ export default class IncomingDataStreamManager {
87
87
  }
88
88
 
89
89
  clearControllers() {
90
+ // Error any in flight streams so readers do not wait forever for chunks that can no longer arrive.
91
+ const abnormalEndError = new DataStreamError(
92
+ 'Room disconnected in the middle of receiving data',
93
+ DataStreamErrorReason.AbnormalEnd,
94
+ );
95
+ for (const { controller } of this.byteStreamControllers.values()) {
96
+ controller.error(abnormalEndError);
97
+ }
98
+ for (const { controller } of this.textStreamControllers.values()) {
99
+ controller.error(abnormalEndError);
100
+ }
90
101
  this.byteStreamControllers.clear();
91
102
  this.textStreamControllers.clear();
92
103
  this.bufferedPackets = [];
@@ -174,11 +185,11 @@ export default class IncomingDataStreamManager {
174
185
  let compressed;
175
186
  switch (streamHeader.compression) {
176
187
  case DataStream_CompressionType.DEFLATE_RAW:
177
- if (!isCompressionStreamSupported()) {
188
+ if (!isDeflateRawCompressionSupported()) {
178
189
  // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
179
190
  // isn't properly abiding by the data streams v2 protocol.
180
191
  log.warn(
181
- `Data stream ${streamHeader.streamId} received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping...`,
192
+ `Data stream ${streamHeader.streamId} received with deflate-raw compression, but this browser does not support deflate-raw decompression. Dropping...`,
182
193
  );
183
194
  return;
184
195
  }
@@ -279,11 +290,11 @@ export default class IncomingDataStreamManager {
279
290
  let compressed;
280
291
  switch (streamHeader.compression) {
281
292
  case DataStream_CompressionType.DEFLATE_RAW:
282
- if (!isCompressionStreamSupported()) {
293
+ if (!isDeflateRawCompressionSupported()) {
283
294
  // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
284
295
  // isn't properly abiding by the data streams v2 protocol.
285
296
  log.warn(
286
- `Data stream ${streamHeader.streamId} received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping...`,
297
+ `Data stream ${streamHeader.streamId} received with deflate-raw compression, but this browser does not support deflate-raw decompression. Dropping...`,
287
298
  );
288
299
  return;
289
300
  }
@@ -5,14 +5,19 @@ import {
5
5
  DataStream_CompressionType,
6
6
  type DataStream_TextHeader,
7
7
  } from '@livekit/protocol';
8
- import { beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
9
  import log from '../../../logger';
10
+ import {
11
+ stubMissingCompressionStreams,
12
+ stubMissingDeflateRawSupport,
13
+ } from '../../../test/compressionStreamStubs';
10
14
  import {
11
15
  CLIENT_PROTOCOL_DATA_STREAM_RPC,
12
16
  CLIENT_PROTOCOL_DATA_STREAM_V2,
13
17
  CLIENT_PROTOCOL_DEFAULT,
14
18
  } from '../../../version';
15
19
  import type RTCEngine from '../../RTCEngine';
20
+ import { STREAM_CHUNK_SIZE_BYTES } from '../constants';
16
21
  import OutgoingDataStreamManager from './OutgoingDataStreamManager';
17
22
 
18
23
  /** Builds a low quality random string of the given length. */
@@ -1195,7 +1200,7 @@ describe('OutgoingDataStreamManager', () => {
1195
1200
  // The split backs off below the MTU rather than bisecting the emoji, so each chunk decodes
1196
1201
  // independently.
1197
1202
  expect(first.content).toHaveLength(14_999);
1198
- const decode = (bytes: Uint8Array) => new TextDecoder('utf-8', { fatal: true }).decode(bytes);
1203
+ const decode = (bytes: Uint8Array) => new TextDecoder('utf-8').decode(bytes);
1199
1204
  expect(() => decode(first.content)).not.toThrow();
1200
1205
  expect(() => decode(second.content)).not.toThrow();
1201
1206
  expect(decode(first.content) + decode(second.content)).toStrictEqual(text);
@@ -1243,50 +1248,66 @@ describe('OutgoingDataStreamManager', () => {
1243
1248
  });
1244
1249
  });
1245
1250
 
1246
- describe('runtime without CompressionStream', () => {
1251
+ describe.each([
1252
+ ['a runtime without CompressionStream', stubMissingCompressionStreams],
1253
+ ['a runtime without deflate-raw support', stubMissingDeflateRawSupport],
1254
+ ])('on %s', (_label, stub) => {
1255
+ let restoreCompressionStreams: () => void;
1256
+
1257
+ beforeEach(() => {
1258
+ restoreCompressionStreams = stub();
1259
+ });
1260
+
1261
+ afterEach(() => {
1262
+ restoreCompressionStreams();
1263
+ });
1264
+
1247
1265
  it('should send raw inline even to compression-capable recipients when the runtime cannot compress', async () => {
1248
- const originalCompressionStream = CompressionStream;
1249
- try {
1250
- (globalThis as any).CompressionStream = undefined;
1266
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1267
+ await manager.sendText('hello hello compressible world', {
1268
+ topic: 'my-topic',
1269
+ destinationIdentities: ['alice'],
1270
+ });
1251
1271
 
1252
- const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1253
- await manager.sendText('hello hello compressible world', {
1254
- topic: 'my-topic',
1255
- destinationIdentities: ['alice'],
1256
- });
1272
+ // Compression eligibility also requires a local compressor; without one, inline still
1273
+ // applies but the payload is sent raw.
1274
+ expect(sentPackets).toHaveLength(1);
1275
+ const header = headerOf(sentPackets[0]);
1276
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1277
+ expect(header.inlineContent).toStrictEqual(
1278
+ new TextEncoder().encode('hello hello compressible world'),
1279
+ );
1280
+ });
1257
1281
 
1258
- // Compression eligibility also requires a local compressor; without one, inline still
1259
- // applies but the payload is sent raw.
1260
- expect(sentPackets).toHaveLength(1);
1261
- const header = headerOf(sentPackets[0]);
1262
- expect(header.compression).toBe(DataStream_CompressionType.NONE);
1263
- expect(header.inlineContent).toStrictEqual(
1264
- new TextEncoder().encode('hello hello compressible world'),
1265
- );
1266
- } finally {
1267
- (globalThis as any).CompressionStream = originalCompressionStream;
1268
- }
1282
+ it('should send BYTES uncompressed when the runtime cannot compress', async () => {
1283
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1284
+ // Larger than the inline budget, so this takes the chunked path rather than riding along in
1285
+ // the header.
1286
+ const bytes = new Uint8Array(20_000).fill(0x07);
1287
+ await manager.sendBytes(bytes, {
1288
+ topic: 'my-topic',
1289
+ destinationIdentities: ['alice'],
1290
+ });
1291
+
1292
+ expect(sentPackets).toHaveLength(4);
1293
+ const header = headerOf(sentPackets[0]);
1294
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1295
+ expect(chunkOf(sentPackets[1]).content).toHaveLength(STREAM_CHUNK_SIZE_BYTES);
1296
+ expect(chunkOf(sentPackets[2]).content).toHaveLength(20_000 - STREAM_CHUNK_SIZE_BYTES);
1269
1297
  });
1270
1298
 
1271
1299
  it('should send a FILE uncompressed when the runtime cannot compress', async () => {
1272
- const originalCompressionStream = CompressionStream;
1273
- try {
1274
- (globalThis as any).CompressionStream = undefined;
1275
-
1276
- const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1277
- const bytes = new Uint8Array(10_000).fill(0x07);
1278
- await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
1279
- topic: 'my-topic',
1280
- destinationIdentities: ['alice'],
1281
- });
1300
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1301
+ const bytes = new Uint8Array(10_000).fill(0x07);
1302
+ await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
1303
+ topic: 'my-topic',
1304
+ destinationIdentities: ['alice'],
1305
+ });
1282
1306
 
1283
- expect(sentPackets).toHaveLength(3);
1284
- const header = headerOf(sentPackets[0]);
1285
- expect(header.compression).toBe(DataStream_CompressionType.NONE);
1286
- expect(chunkOf(sentPackets[1]).content).toHaveLength(10_000);
1287
- } finally {
1288
- (globalThis as any).CompressionStream = originalCompressionStream;
1289
- }
1307
+ expect(sentPackets).toHaveLength(3);
1308
+ const header = headerOf(sentPackets[0]);
1309
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1310
+ expect(chunkOf(sentPackets[1]).content).toHaveLength(10_000);
1290
1311
  });
1291
1312
  });
1292
1313
  });
@@ -24,7 +24,7 @@ import type {
24
24
  TextStreamInfo,
25
25
  } from '../../types';
26
26
  import {
27
- isCompressionStreamSupported,
27
+ isDeflateRawCompressionSupported,
28
28
  numberToBigInt,
29
29
  readBytesInChunks,
30
30
  readableFromBytes,
@@ -101,7 +101,7 @@ export default class OutgoingDataStreamManager {
101
101
 
102
102
  const compressEligible =
103
103
  compress &&
104
- isCompressionStreamSupported() &&
104
+ isDeflateRawCompressionSupported() &&
105
105
  this.allRecipientsSupportV2(options?.destinationIdentities) &&
106
106
  this.allRecipientsSupportCompression(options?.destinationIdentities);
107
107
  let compressedStream = compressEligible
@@ -239,7 +239,7 @@ export default class OutgoingDataStreamManager {
239
239
 
240
240
  const compressEligible =
241
241
  compress &&
242
- isCompressionStreamSupported() &&
242
+ isDeflateRawCompressionSupported() &&
243
243
  this.allRecipientsSupportV2(destinationIdentities) &&
244
244
  this.allRecipientsSupportCompression(destinationIdentities);
245
245
  let compressedStream = compressEligible
@@ -463,7 +463,7 @@ export default class OutgoingDataStreamManager {
463
463
  const destinationIdentities = options?.destinationIdentities;
464
464
  const compress =
465
465
  (options?.compress ?? true) &&
466
- isCompressionStreamSupported() &&
466
+ isDeflateRawCompressionSupported() &&
467
467
  this.allRecipientsSupportV2(destinationIdentities) &&
468
468
  this.allRecipientsSupportCompression(destinationIdentities);
469
469
 
@@ -47,6 +47,7 @@ export const roomOptionDefaults: InternalRoomOptions = {
47
47
 
48
48
  export const roomConnectOptionDefaults: InternalRoomConnectOptions = {
49
49
  autoSubscribe: true,
50
+ disableIceLite: false,
50
51
  maxRetries: 1,
51
52
  peerConnectionTimeout: 15_000,
52
53
  websocketTimeout: 15_000,
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ConnectionError, canFailOverToAnotherRegion } from './errors';
3
+
4
+ describe('canFailOverToAnotherRegion', () => {
5
+ it('allows failover on 403, which LiveKit Cloud uses to signal region pinning', () => {
6
+ // The RTC paths 403 when the project is not allowed in the region the client geo-routed to.
7
+ // `/settings/regions` stays reachable so the client can discover where it is allowed.
8
+ expect(
9
+ canFailOverToAnotherRegion(
10
+ ConnectionError.notAllowed('project not allowed in this region.', 403),
11
+ ),
12
+ ).toBe(true);
13
+ });
14
+
15
+ it('does not allow failover on 401 — no other region will accept the same token', () => {
16
+ expect(canFailOverToAnotherRegion(ConnectionError.notAllowed('unauthorized', 401))).toBe(false);
17
+ });
18
+
19
+ it('does not allow failover when the room does not exist, reported as NotAllowed with 404', () => {
20
+ expect(
21
+ canFailOverToAnotherRegion(ConnectionError.notAllowed('requested room does not exist', 404)),
22
+ ).toBe(false);
23
+ });
24
+
25
+ it('does not allow failover on a cancelled attempt', () => {
26
+ expect(canFailOverToAnotherRegion(ConnectionError.cancelled('aborted'))).toBe(false);
27
+ });
28
+
29
+ it.each([
30
+ ['server unreachable', ConnectionError.serverUnreachable('unreachable')],
31
+ ['timeout', ConnectionError.timeout('timed out')],
32
+ ['internal', ConnectionError.internal('internal')],
33
+ ])('allows failover on a %s error', (_name, error) => {
34
+ expect(canFailOverToAnotherRegion(error)).toBe(true);
35
+ });
36
+ });
@@ -188,6 +188,33 @@ export class ConnectionError<
188
188
  }
189
189
  }
190
190
 
191
+ /**
192
+ * Whether a failed connection attempt may be retried against a different LiveKit Cloud region.
193
+ *
194
+ * LiveKit Cloud signals project-level region pinning by returning 403 on the RTC paths when the
195
+ * project is not allowed in the region the client geo-routed to. `/settings/regions` is
196
+ * deliberately left reachable so the client can discover its allowed regions and connect there, so
197
+ * a 403 must not be treated as terminal.
198
+ *
199
+ * A 401 stays terminal: no other region will accept a token this one rejected. So does the 404
200
+ * "requested room does not exist" case, which is also reported as `NotAllowed`.
201
+ *
202
+ * We key on the status rather than the server's error message because that message is an
203
+ * unversioned human-readable string; matching it would let a copy edit break already-shipped
204
+ * clients. If a 403 really was a permissions failure rather than region pinning, every region
205
+ * attempt fails the same way and the original error still surfaces — at the cost of one extra
206
+ * region lookup.
207
+ */
208
+ export function canFailOverToAnotherRegion(error: ConnectionError): boolean {
209
+ if (error.reason === ConnectionErrorReason.Cancelled) {
210
+ return false;
211
+ }
212
+ if (error.reason === ConnectionErrorReason.NotAllowed) {
213
+ return error.status === 403;
214
+ }
215
+ return true;
216
+ }
217
+
191
218
  export class DeviceUnsupportedError extends LivekitError {
192
219
  readonly name = 'DeviceUnsupportedError';
193
220
 
@@ -225,7 +225,7 @@ export enum RoomEvent {
225
225
  * Data packets provides the ability to use LiveKit to send/receive arbitrary payloads.
226
226
  * All participants in the room will receive the messages sent to the room.
227
227
  *
228
- * args: (payload: NonSharedUint8Array, participant: [[Participant]], kind: [[DataPacket_Kind]], topic?: string)
228
+ * args: (payload: NonSharedUint8Array, participant: [[Participant]] | undefined, kind: [[DataPacket_Kind]], topic?: string, encryptionType?: Encryption_Type, participantIdentity?: string)
229
229
  */
230
230
  DataReceived = 'dataReceived',
231
231
 
@@ -633,6 +633,7 @@ export enum EngineEvent {
633
633
  Joined = 'joined',
634
634
  TokenRefreshed = 'tokenRefreshed',
635
635
  ServerRegionsReported = 'serverRegionsReported',
636
+ RequestSubscribedCodecRefresh = 'requestSubscribedCodecRefresh',
636
637
  }
637
638
 
638
639
  export enum TrackEvent {
@@ -1,6 +1,12 @@
1
- import { PacketTrailerFeature } from '@livekit/protocol';
1
+ import {
2
+ type AddTrackRequest,
3
+ Encryption_Type,
4
+ PacketTrailerFeature,
5
+ VideoQuality,
6
+ } from '@livekit/protocol';
2
7
  import { describe, expect, it, vi } from 'vitest';
3
8
  import type LocalTrack from '../track/LocalTrack';
9
+ import type LocalVideoTrack from '../track/LocalVideoTrack';
4
10
  import { Track } from '../track/Track';
5
11
  import type { TrackPublishOptions } from '../track/options';
6
12
  import LocalParticipant from './LocalParticipant';
@@ -79,3 +85,132 @@ describe('LocalParticipant frame metadata publish options', () => {
79
85
  expect(participant.log.warn).toHaveBeenCalledOnce();
80
86
  });
81
87
  });
88
+
89
+ type BackupCodecTestParticipant = {
90
+ encryptionType: Encryption_Type;
91
+ trackPublications: Map<string, { track: LocalVideoTrack }>;
92
+ roomOptions: { publishDefaults: TrackPublishOptions };
93
+ engine: {
94
+ isClosed: boolean;
95
+ addTrack: ReturnType<typeof vi.fn>;
96
+ createSimulcastSender: ReturnType<typeof vi.fn>;
97
+ negotiate: ReturnType<typeof vi.fn>;
98
+ };
99
+ log: { warn: ReturnType<typeof vi.fn>; debug: ReturnType<typeof vi.fn> };
100
+ publishAdditionalCodecForTrack: LocalParticipant['publishAdditionalCodecForTrack'];
101
+ };
102
+
103
+ // a resizing processor makes mediaStreamTrack (processed) and dimensions (raw capture) differ
104
+ function makeBackupCodecTrack(
105
+ settings: MediaTrackSettings,
106
+ source = Track.Source.Camera,
107
+ captureSettings = settings,
108
+ ) {
109
+ return {
110
+ isLocal: true,
111
+ kind: Track.Kind.Video,
112
+ sid: 'TR_video',
113
+ source,
114
+ isMuted: false,
115
+ mediaStreamID: 'stream-id',
116
+ mediaStreamTrack: { enabled: true, id: 'primary-cid', getSettings: () => settings },
117
+ get dimensions() {
118
+ const { width, height } = captureSettings;
119
+ return width && height ? { width, height } : undefined;
120
+ },
121
+ addSimulcastTrack: () => ({ mediaStreamTrack: { id: 'backup-cid' } }),
122
+ } as unknown as LocalVideoTrack;
123
+ }
124
+
125
+ function makeBackupCodecParticipant(track: LocalVideoTrack) {
126
+ const participant = Object.create(LocalParticipant.prototype) as BackupCodecTestParticipant;
127
+ // Object.create runs no field initializers, so without this the encryption guard sees
128
+ // undefined and the method returns before publishing anything.
129
+ participant.encryptionType = Encryption_Type.NONE;
130
+ participant.trackPublications = new Map([['TR_video', { track }]]);
131
+ participant.roomOptions = {
132
+ publishDefaults: { videoCodec: 'vp9', backupCodec: { codec: 'vp8' }, simulcast: true },
133
+ };
134
+ participant.engine = {
135
+ isClosed: false,
136
+ addTrack: vi.fn(async () => ({})),
137
+ createSimulcastSender: vi.fn(async () => {}),
138
+ negotiate: vi.fn(async () => {}),
139
+ };
140
+ participant.log = { warn: vi.fn(), debug: vi.fn() };
141
+ return participant;
142
+ }
143
+
144
+ function capturedRequest(participant: BackupCodecTestParticipant): AddTrackRequest {
145
+ expect(participant.engine.addTrack).toHaveBeenCalledOnce();
146
+ return participant.engine.addTrack.mock.calls[0][0] as AddTrackRequest;
147
+ }
148
+
149
+ describe('LocalParticipant.publishAdditionalCodecForTrack', () => {
150
+ it('sets the capture dimensions and the backup codec layers', async () => {
151
+ const track = makeBackupCodecTrack({ width: 1280, height: 720 });
152
+ const participant = makeBackupCodecParticipant(track);
153
+
154
+ await participant.publishAdditionalCodecForTrack(track, 'vp8');
155
+
156
+ const req = capturedRequest(participant);
157
+ expect(req.width).toBe(1280);
158
+ expect(req.height).toBe(720);
159
+ expect(req.layers.length).toBeGreaterThan(0);
160
+ for (const layer of req.layers) {
161
+ expect(layer.width).toBeGreaterThan(0);
162
+ expect(layer.height).toBeGreaterThan(0);
163
+ }
164
+ expect(Math.max(...req.layers.map((layer) => layer.width))).toBe(1280);
165
+ expect(req.simulcastCodecs[0].codec).toBe('vp8');
166
+ expect(req.simulcastCodecs[0].layers).toEqual(req.layers);
167
+ expect(participant.log.warn).not.toHaveBeenCalled();
168
+ });
169
+
170
+ // screenshare disables simulcast for the backup codec, which collapses the encodings to a
171
+ // single unconstrained one and so to a single full size layer
172
+ it('sends one full size layer for a screenshare track', async () => {
173
+ const track = makeBackupCodecTrack({ width: 1280, height: 720 }, Track.Source.ScreenShare);
174
+ const participant = makeBackupCodecParticipant(track);
175
+
176
+ await participant.publishAdditionalCodecForTrack(track, 'vp8');
177
+
178
+ const req = capturedRequest(participant);
179
+ expect(req.layers).toHaveLength(1);
180
+ expect(req.layers[0].width).toBe(1280);
181
+ expect(req.layers[0].height).toBe(720);
182
+ expect(req.layers[0].quality).toBe(VideoQuality.HIGH);
183
+ expect(req.simulcastCodecs[0].layers).toEqual(req.layers);
184
+ expect(participant.log.warn).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it('sizes layers from the processed track when a processor resizes', async () => {
188
+ const track = makeBackupCodecTrack({ width: 640, height: 360 }, Track.Source.Camera, {
189
+ width: 1280,
190
+ height: 720,
191
+ });
192
+ const participant = makeBackupCodecParticipant(track);
193
+
194
+ await participant.publishAdditionalCodecForTrack(track, 'vp8');
195
+
196
+ const req = capturedRequest(participant);
197
+ expect(req.width).toBe(640);
198
+ expect(req.height).toBe(360);
199
+ expect(Math.max(...req.layers.map((layer) => layer.width))).toBe(640);
200
+ expect(req.simulcastCodecs[0].layers).toEqual(req.layers);
201
+ });
202
+
203
+ it('omits layers when the capture dimensions are unknown', async () => {
204
+ const track = makeBackupCodecTrack({});
205
+ const participant = makeBackupCodecParticipant(track);
206
+
207
+ await participant.publishAdditionalCodecForTrack(track, 'vp8');
208
+
209
+ const req = capturedRequest(participant);
210
+ expect(req.width).toBe(0);
211
+ expect(req.height).toBe(0);
212
+ expect(req.layers).toEqual([]);
213
+ expect(req.simulcastCodecs[0].layers).toEqual([]);
214
+ expect(participant.log.warn).toHaveBeenCalledOnce();
215
+ });
216
+ });