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
@@ -1,13 +1,26 @@
1
1
  import { ClientInfo_Capability } from '@livekit/protocol';
2
2
  import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import {
4
+ stubMissingCompressionStreams,
5
+ stubMissingDeflateRawSupport,
6
+ } from '../test/compressionStreamStubs';
7
+ import { getBrowser } from '../utils/browserParser';
3
8
  import {
4
9
  ddExtensionURI,
5
10
  extractMaxAgeFromRequestHeaders,
6
11
  getClientInfo,
12
+ isAppleMobile,
13
+ isCompressionStreamSupported,
14
+ isDeflateRawCompressionSupported,
15
+ isIPadOS,
16
+ isSVCSimulcast,
17
+ isSVCSimulcastSupportedByServer,
7
18
  negotiateDependencyDescriptor,
19
+ resetDeflateRawCompressionSupportCache,
8
20
  splitUtf8,
9
21
  supportsAdaptiveStream,
10
22
  toWebsocketUrl,
23
+ usesLegacySVCEncodings,
11
24
  } from './utils';
12
25
 
13
26
  describe('toWebsocketUrl', () => {
@@ -280,3 +293,201 @@ describe('supportsAdaptiveStream', () => {
280
293
  expect(supportsAdaptiveStream()).toBe(false);
281
294
  });
282
295
  });
296
+
297
+ describe('isSVCSimulcast', () => {
298
+ it('requires an svc capable codec, simulcast and a single spatial layer mode', () => {
299
+ expect(isSVCSimulcast('vp9', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(true);
300
+ expect(isSVCSimulcast('av1', { simulcast: true, scalabilityMode: 'L1T3' })).toBe(true);
301
+ });
302
+
303
+ it('stays on svc without the opt in', () => {
304
+ expect(isSVCSimulcast('vp9', { simulcast: false, scalabilityMode: 'L1T2' })).toBe(false);
305
+ expect(isSVCSimulcast('vp9', { simulcast: true })).toBe(false);
306
+ // a multi spatial layer mode is svc by definition
307
+ expect(isSVCSimulcast('vp9', { simulcast: true, scalabilityMode: 'L3T3_KEY' })).toBe(false);
308
+ expect(isSVCSimulcast('vp9', undefined)).toBe(false);
309
+ });
310
+
311
+ it('does not apply to non svc codecs', () => {
312
+ expect(isSVCSimulcast('vp8', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
313
+ expect(isSVCSimulcast('h264', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
314
+ expect(isSVCSimulcast(undefined, { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
315
+ });
316
+ });
317
+
318
+ describe('isSVCSimulcastSupportedByServer', () => {
319
+ it('requires a server newer than 1.13.6', () => {
320
+ expect(isSVCSimulcastSupportedByServer('1.13.7')).toBe(true);
321
+ expect(isSVCSimulcastSupportedByServer('1.14.0')).toBe(true);
322
+ expect(isSVCSimulcastSupportedByServer('2.0.0')).toBe(true);
323
+ });
324
+
325
+ it('rejects 1.13.6 and older', () => {
326
+ expect(isSVCSimulcastSupportedByServer('1.13.6')).toBe(false);
327
+ expect(isSVCSimulcastSupportedByServer('1.13.5')).toBe(false);
328
+ expect(isSVCSimulcastSupportedByServer('1.9.0')).toBe(false);
329
+ expect(isSVCSimulcastSupportedByServer('0.15.1')).toBe(false);
330
+ });
331
+
332
+ it('treats an unknown version as unsupported', () => {
333
+ expect(isSVCSimulcastSupportedByServer(undefined)).toBe(false);
334
+ expect(isSVCSimulcastSupportedByServer('')).toBe(false);
335
+ });
336
+ });
337
+
338
+ describe('usesLegacySVCEncodings', () => {
339
+ const stubUserAgent = (userAgent: string) =>
340
+ vi.stubGlobal('navigator', { userAgent, product: 'Gecko' });
341
+
342
+ afterEach(() => {
343
+ vi.unstubAllGlobals();
344
+ });
345
+
346
+ const chrome = (v: string) =>
347
+ `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v} Safari/537.36`;
348
+
349
+ it('is legacy on chrome before M113', () => {
350
+ stubUserAgent(chrome('112.0.0.0'));
351
+ expect(usesLegacySVCEncodings()).toBe(true);
352
+ });
353
+
354
+ it('is not legacy from chrome M113 onwards', () => {
355
+ stubUserAgent(chrome('113.0.0.0'));
356
+ expect(usesLegacySVCEncodings()).toBe(false);
357
+ stubUserAgent(chrome('120.0.0.0'));
358
+ expect(usesLegacySVCEncodings()).toBe(false);
359
+ });
360
+
361
+ it('is legacy on safari, which has no rid based svc simulcast', () => {
362
+ stubUserAgent(
363
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
364
+ );
365
+ expect(usesLegacySVCEncodings()).toBe(true);
366
+ });
367
+
368
+ it('is legacy on iOS, where every browser is webkit', () => {
369
+ stubUserAgent(
370
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1',
371
+ );
372
+ expect(usesLegacySVCEncodings()).toBe(true);
373
+ });
374
+
375
+ it('is legacy on react native regardless of chrome version', () => {
376
+ vi.stubGlobal('navigator', { userAgent: chrome('120.0.0.0'), product: 'ReactNative' });
377
+ expect(usesLegacySVCEncodings()).toBe(true);
378
+ });
379
+ });
380
+
381
+ describe('isIPadOS / isAppleMobile', () => {
382
+ const stub = (userAgent: string, maxTouchPoints: number) =>
383
+ vi.stubGlobal('navigator', { userAgent, maxTouchPoints, product: 'Gecko' });
384
+
385
+ afterEach(() => {
386
+ vi.unstubAllGlobals();
387
+ });
388
+
389
+ // measured on an iPad 6 running iPadOS 17: the default (desktop) user agent, maxTouchPoints 5
390
+ const IPAD_DESKTOP_UA =
391
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.14 Safari/605.1.15';
392
+ // measured on macOS Safari 26.4: the same shape of user agent, maxTouchPoints 0
393
+ const MAC_SAFARI_UA =
394
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Safari/605.1.15';
395
+ const IPHONE_UA =
396
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1';
397
+ const IPAD_MOBILE_UA =
398
+ 'Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1';
399
+ const MAC_CHROME_UA =
400
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36';
401
+ const ANDROID_CHROME_UA =
402
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36';
403
+
404
+ it('detects an iPad sending the desktop user agent', () => {
405
+ stub(IPAD_DESKTOP_UA, 5);
406
+ expect(isIPadOS()).toBe(true);
407
+ expect(isAppleMobile()).toBe(true);
408
+ });
409
+
410
+ it('is the case getBrowser() alone gets wrong', () => {
411
+ // the gap this exists to close: iPadOS parses as macOS, so `os === 'iOS'` misses it
412
+ stub(IPAD_DESKTOP_UA, 5);
413
+ expect(getBrowser()?.os).toBe('macOS');
414
+ });
415
+
416
+ it('leaves macOS Safari alone, where the camera reports correctly and frames are slow', () => {
417
+ stub(MAC_SAFARI_UA, 0);
418
+ expect(isIPadOS()).toBe(false);
419
+ expect(isAppleMobile()).toBe(false);
420
+ });
421
+
422
+ it('covers iPhones through the existing iOS check', () => {
423
+ stub(IPHONE_UA, 5);
424
+ expect(isIPadOS()).toBe(false);
425
+ expect(isAppleMobile()).toBe(true);
426
+ });
427
+
428
+ it('covers an iPad set to request the mobile site', () => {
429
+ stub(IPAD_MOBILE_UA, 5);
430
+ expect(isIPadOS()).toBe(false);
431
+ expect(isAppleMobile()).toBe(true);
432
+ });
433
+
434
+ it('does not fire for a Mac with a touchscreen attached, which is not Safari', () => {
435
+ stub(MAC_CHROME_UA, 5);
436
+ expect(isIPadOS()).toBe(false);
437
+ expect(isAppleMobile()).toBe(false);
438
+ });
439
+
440
+ it('does not fire on android', () => {
441
+ stub(ANDROID_CHROME_UA, 5);
442
+ expect(isIPadOS()).toBe(false);
443
+ expect(isAppleMobile()).toBe(false);
444
+ });
445
+
446
+ it('does not fire on a desktop safari reporting no touch support', () => {
447
+ stub(MAC_SAFARI_UA, 1);
448
+ expect(isIPadOS()).toBe(false);
449
+ });
450
+ });
451
+
452
+ describe('isDeflateRawCompressionSupported', () => {
453
+ afterEach(() => {
454
+ resetDeflateRawCompressionSupportCache();
455
+ });
456
+
457
+ it('is true in a runtime that supports the deflate-raw format', () => {
458
+ expect(isDeflateRawCompressionSupported()).toBe(true);
459
+ });
460
+
461
+ it('is false without the compression streams api at all', () => {
462
+ const restoreCompressionStreams = stubMissingCompressionStreams();
463
+ try {
464
+ expect(isDeflateRawCompressionSupported()).toBe(false);
465
+ expect(isCompressionStreamSupported()).toBe(false);
466
+ } finally {
467
+ restoreCompressionStreams();
468
+ }
469
+ });
470
+
471
+ it('is false when the api exists but deflate-raw is not a supported format', () => {
472
+ const restoreCompressionStreams = stubMissingDeflateRawSupport();
473
+ try {
474
+ expect(isDeflateRawCompressionSupported()).toBe(false);
475
+ // Chromium 80-102 still compresses gzip, which is all the wrapped join request needs.
476
+ expect(isCompressionStreamSupported()).toBe(true);
477
+ } finally {
478
+ restoreCompressionStreams();
479
+ }
480
+ });
481
+
482
+ it('probes the runtime only once', () => {
483
+ expect(isDeflateRawCompressionSupported()).toBe(true);
484
+
485
+ const originalCompressionStream = globalThis.CompressionStream;
486
+ try {
487
+ (globalThis as any).CompressionStream = undefined;
488
+ expect(isDeflateRawCompressionSupported()).toBe(true);
489
+ } finally {
490
+ (globalThis as any).CompressionStream = originalCompressionStream;
491
+ }
492
+ });
493
+ });
package/src/room/utils.ts CHANGED
@@ -185,6 +185,65 @@ export function negotiateDependencyDescriptor(transceiver: RTCRtpTransceiver): b
185
185
  }
186
186
  }
187
187
 
188
+ /**
189
+ * VP9 and AV1 are published as SVC (a single RTP stream carrying every spatial layer)
190
+ * by default. They can instead be published as real, rid based simulcast — one
191
+ * independent stream per rid, each carrying a single spatial layer — when the caller
192
+ * opts in with `simulcast: true` and a single spatial layer scalability mode (`L1Tx`).
193
+ *
194
+ * The SFU has to be told about this: without an explicit
195
+ * `SimulcastCodec.videoLayerMode` it assumes `MULTIPLE_SPATIAL_LAYERS_PER_STREAM` for
196
+ * any SVC capable codec.
197
+ */
198
+ export function isSVCSimulcast(
199
+ codec?: string,
200
+ options?: { simulcast?: boolean; scalabilityMode?: string },
201
+ ): boolean {
202
+ return isSVCCodec(codec) && !!options?.simulcast && !!options.scalabilityMode?.startsWith('L1T');
203
+ }
204
+
205
+ /**
206
+ * Whether the browser reads multiple encodings on an SVC capable codec as *legacy SVC*
207
+ * rather than as real simulcast.
208
+ *
209
+ * Before Chrome M113, supplying more than one encoding for VP9/AV1 selected SVC mode;
210
+ * only from M113 does libwebrtc treat such encodings as simulcast, and only when each
211
+ * one carries its own scalabilityMode. Safari (and anything WebKit based, i. e. every
212
+ * browser on iOS) still uses the old interpretation, as does React Native's libwebrtc.
213
+ * Announced at https://groups.google.com/g/discuss-webrtc/c/-QQ3pxrl-fw
214
+ *
215
+ * Where this is true the rids would not exist on the wire, so VP9/AV1 must be published
216
+ * as SVC no matter what the caller asked for.
217
+ */
218
+ export function usesLegacySVCEncodings(): boolean {
219
+ const browser = getBrowser();
220
+ return (
221
+ isSafariBased() ||
222
+ // Even tho RN runs M114, it does not produce SVC layers when a single encoding
223
+ // is provided. So we'll use the legacy SVC specification for now.
224
+ // TODO: when we upstream libwebrtc, this will need additional verification
225
+ isReactNative() ||
226
+ (browser?.name === 'Chrome' && compareVersions(browser.version, '113') < 0)
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Last server version that doesn't support vp9/av1 simulcast.
232
+ */
233
+ const svcSimulcastMinServerVersion = '1.13.6';
234
+
235
+ /**
236
+ * Whether the connected server honours `SimulcastCodec.videoLayerMode`, i. e. whether
237
+ * VP9/AV1 can be published as rid based simulcast. An unknown version is treated as
238
+ * unsupported so the publish falls back to SVC.
239
+ */
240
+ export function isSVCSimulcastSupportedByServer(serverVersion?: string): boolean {
241
+ if (!serverVersion) {
242
+ return false;
243
+ }
244
+ return compareVersions(serverVersion, svcSimulcastMinServerVersion) > 0;
245
+ }
246
+
188
247
  export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
189
248
  if (!document || isSafariBased()) {
190
249
  return false;
@@ -241,6 +300,33 @@ export function isSafariBased(): boolean {
241
300
  return b?.name === 'Safari' || b?.os === 'iOS';
242
301
  }
243
302
 
303
+ /**
304
+ * iPadOS has sent the desktop Macintosh user agent by default since iPadOS 13, so `getBrowser()`
305
+ * reports `os: 'macOS'` there and every `os === 'iOS'` check misses it. macOS Safari implements no
306
+ * touch events at all (`TouchEvent` undefined, `maxTouchPoints` 0) regardless of any touchscreen
307
+ * attached, so touch capability separates the two. Measured on an iPad 6 / iPadOS 17
308
+ * (`maxTouchPoints` 5) against macOS Safari 26.4 (`maxTouchPoints` 0).
309
+ *
310
+ * Deliberately a separate helper rather than a fix in `browserParser`: iPadOS spoofs the Mac
311
+ * version too, so reporting `os: 'iOS'` there would leave `osVersion` at `10.15.7` and silently
312
+ * break {@link isSafari17Based} and {@link isSafariSvcApi}, which compare it.
313
+ */
314
+ export function isIPadOS(): boolean {
315
+ if (!isWeb()) {
316
+ return false;
317
+ }
318
+ const b = getBrowser();
319
+ return b?.name === 'Safari' && b?.os === 'macOS' && navigator.maxTouchPoints > 1;
320
+ }
321
+
322
+ /**
323
+ * iPhone, iPad and iOS-hosted browsers — everything running the Apple camera capture pipeline,
324
+ * including iPads that present themselves as a Mac.
325
+ */
326
+ export function isAppleMobile(): boolean {
327
+ return getBrowser()?.os === 'iOS' || isIPadOS();
328
+ }
329
+
244
330
  export function isSafari17Based(): boolean {
245
331
  const b = getBrowser();
246
332
  return (
@@ -902,6 +988,49 @@ export function isCompressionStreamSupported() {
902
988
  return typeof CompressionStream !== 'undefined';
903
989
  }
904
990
 
991
+ let deflateRawCompressionSupported: boolean | null = null;
992
+
993
+ /**
994
+ * Whether this runtime can compress AND decompress the `deflate-raw` format, which is a stricter
995
+ * requirement than {@link isCompressionStreamSupported}: Chromium 80-102 included
996
+ * `CompressionStream` supporting only `gzip` / `deflate`, and `deflate-raw` was added in Chromium
997
+ * 103 (Firefox 113, Safari 16.4).
998
+ */
999
+ export function isDeflateRawCompressionSupported(): boolean {
1000
+ if (typeof deflateRawCompressionSupported !== 'boolean') {
1001
+ deflateRawCompressionSupported = probeDeflateRawCompressionSupport();
1002
+ }
1003
+ return deflateRawCompressionSupported;
1004
+ }
1005
+
1006
+ function probeDeflateRawCompressionSupport(): boolean {
1007
+ if (typeof CompressionStream === 'undefined' || typeof DecompressionStream === 'undefined') {
1008
+ return false;
1009
+ }
1010
+ try {
1011
+ // There is no api to query format support, and constructing with an unsupported format throws
1012
+ // synchronously, so constructing one of each is the probe. Both are checked because the
1013
+ // deflate-raw capability advertised to peers is as much about decompressing what they send us
1014
+ // as it is about compressing what we send them.
1015
+ new CompressionStream('deflate-raw');
1016
+ new DecompressionStream('deflate-raw');
1017
+ return true;
1018
+ } catch {
1019
+ return false;
1020
+ }
1021
+ }
1022
+
1023
+ /**
1024
+ * Clears the memoized {@link isDeflateRawCompressionSupported} probe result. A runtime doesn't
1025
+ * gain or lose compression formats while it is running, so this only exists for tests that swap
1026
+ * the global `CompressionStream` / `DecompressionStream`.
1027
+ *
1028
+ * @internal
1029
+ */
1030
+ export function resetDeflateRawCompressionSupportCache() {
1031
+ deflateRawCompressionSupported = null;
1032
+ }
1033
+
905
1034
  export function isPublisherOfferWithJoinSupported() {
906
1035
  // we have connectivity issue about publisher offer with join on firefox #1919
907
1036
  return isCompressionStreamSupported() && !isFireFox();
@@ -0,0 +1,53 @@
1
+ import { resetDeflateRawCompressionSupportCache } from '../room/utils';
2
+
3
+ /**
4
+ * Swaps the global `CompressionStream` / `DecompressionStream` away entirely, emulating a runtime
5
+ * that predates the compression streams api. Returns a function that restores them.
6
+ */
7
+ export function stubMissingCompressionStreams(): () => void {
8
+ return stubCompressionStreams(undefined, undefined);
9
+ }
10
+
11
+ /**
12
+ * Swaps in global `CompressionStream` / `DecompressionStream` constructors that reject the
13
+ * `deflate-raw` format the way Chromium 80-102 does (the api exists, but only `gzip` / `deflate`
14
+ * are supported). Returns a function that restores the real ones.
15
+ */
16
+ export function stubMissingDeflateRawSupport(): () => void {
17
+ return stubCompressionStreams(
18
+ legacyFormatsOnly('CompressionStream', CompressionStream),
19
+ legacyFormatsOnly('DecompressionStream', DecompressionStream),
20
+ );
21
+ }
22
+
23
+ type CompressionStreamCtor<T> = new (format: CompressionFormat) => T;
24
+
25
+ function legacyFormatsOnly<T>(
26
+ name: string,
27
+ Real: CompressionStreamCtor<T>,
28
+ ): CompressionStreamCtor<T> {
29
+ return function LegacyCompressionStream(format: CompressionFormat) {
30
+ if (format === 'deflate-raw') {
31
+ throw new TypeError(
32
+ `Failed to construct '${name}': Unsupported compression format: 'deflate-raw'`,
33
+ );
34
+ }
35
+ return new Real(format);
36
+ } as unknown as CompressionStreamCtor<T>;
37
+ }
38
+
39
+ function stubCompressionStreams(compression: unknown, decompression: unknown): () => void {
40
+ const originalCompressionStream = globalThis.CompressionStream;
41
+ const originalDecompressionStream = globalThis.DecompressionStream;
42
+ (globalThis as any).CompressionStream = compression;
43
+ (globalThis as any).DecompressionStream = decompression;
44
+ // The support probe memoizes, which is only safe because a real runtime never changes formats
45
+ // mid-session; swapping the globals has to invalidate it.
46
+ resetDeflateRawCompressionSupportCache();
47
+
48
+ return () => {
49
+ (globalThis as any).CompressionStream = originalCompressionStream;
50
+ (globalThis as any).DecompressionStream = originalDecompressionStream;
51
+ resetDeflateRawCompressionSupportCache();
52
+ };
53
+ }