livekit-client 2.22.3 → 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 (214) 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 +2 -1
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +602 -247
  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/options.d.ts +14 -0
  17. package/dist/src/options.d.ts.map +1 -1
  18. package/dist/src/room/PCTransport.d.ts +78 -7
  19. package/dist/src/room/PCTransport.d.ts.map +1 -1
  20. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +8 -0
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts +1 -1
  24. package/dist/src/room/Room.d.ts.map +1 -1
  25. package/dist/src/room/defaults.d.ts.map +1 -1
  26. package/dist/src/room/errors.d.ts +18 -0
  27. package/dist/src/room/errors.d.ts.map +1 -1
  28. package/dist/src/room/events.d.ts +3 -2
  29. package/dist/src/room/events.d.ts.map +1 -1
  30. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  31. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/LocalVideoTrack.d.ts +2 -0
  34. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  35. package/dist/src/room/track/utils.d.ts +13 -0
  36. package/dist/src/room/track/utils.d.ts.map +1 -1
  37. package/dist/src/room/utils.d.ts +32 -0
  38. package/dist/src/room/utils.d.ts.map +1 -1
  39. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  40. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  41. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  42. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  43. package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
  44. package/dist/ts4.2/api/utils.d.ts +9 -9
  45. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  46. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  47. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  48. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  49. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  50. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  51. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  52. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  53. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  54. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  55. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
  56. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  57. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  58. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  59. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  60. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  61. package/dist/ts4.2/e2ee/types.d.ts +231 -231
  62. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  63. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  64. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
  65. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
  66. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  67. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  68. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
  69. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  70. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  71. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  72. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  73. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  74. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  75. package/dist/ts4.2/index.d.ts +76 -76
  76. package/dist/ts4.2/logger.d.ts +66 -66
  77. package/dist/ts4.2/options.d.ts +163 -149
  78. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  79. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  80. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  81. package/dist/ts4.2/room/PCTransport.d.ts +246 -175
  82. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  83. package/dist/ts4.2/room/RTCEngine.d.ts +269 -262
  84. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  85. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  86. package/dist/ts4.2/room/Room.d.ts +354 -365
  87. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  88. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
  89. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
  90. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  91. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  92. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  93. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  94. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  95. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  96. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  97. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  98. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  99. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  100. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  101. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  102. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  103. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  104. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  105. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  106. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  107. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  108. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  109. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  110. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  111. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  112. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  113. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  114. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  115. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  116. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  117. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  118. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  119. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  120. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  121. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  122. package/dist/ts4.2/room/debounce.d.ts +11 -11
  123. package/dist/ts4.2/room/defaults.d.ts +9 -9
  124. package/dist/ts4.2/room/errors.d.ts +180 -162
  125. package/dist/ts4.2/room/events.d.ts +638 -637
  126. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
  127. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  128. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  129. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  130. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
  131. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  132. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  133. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  134. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  135. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  136. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  137. package/dist/ts4.2/room/stats.d.ts +71 -71
  138. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  139. package/dist/ts4.2/room/timers.d.ts +12 -12
  140. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  141. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  142. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  143. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  144. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
  145. package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
  146. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  147. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -100
  148. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
  149. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  150. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  151. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  152. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  153. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  154. package/dist/ts4.2/room/track/create.d.ts +25 -25
  155. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  156. package/dist/ts4.2/room/track/options.d.ts +403 -403
  157. package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
  158. package/dist/ts4.2/room/track/record.d.ts +13 -13
  159. package/dist/ts4.2/room/track/types.d.ts +28 -28
  160. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  161. package/dist/ts4.2/room/types.d.ts +127 -127
  162. package/dist/ts4.2/room/utils.d.ts +246 -214
  163. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  164. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  165. package/dist/ts4.2/test/mocks.d.ts +12 -12
  166. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  167. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  168. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  169. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  170. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  171. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  172. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  173. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  174. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  175. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  176. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  177. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  178. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  179. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  180. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  181. package/dist/ts4.2/version.d.ts +15 -15
  182. package/package.json +5 -4
  183. package/src/api/SignalClient.e2e.test.ts +1 -3
  184. package/src/api/SignalClient.test.ts +160 -10
  185. package/src/api/SignalClient.ts +75 -54
  186. package/src/options.ts +16 -0
  187. package/src/room/PCTransport.test.ts +183 -7
  188. package/src/room/PCTransport.ts +197 -36
  189. package/src/room/PCTransportManager.ts +3 -1
  190. package/src/room/RTCEngine.test.ts +84 -0
  191. package/src/room/RTCEngine.ts +24 -5
  192. package/src/room/Room.test.ts +32 -6
  193. package/src/room/Room.ts +26 -6
  194. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
  195. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
  196. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  197. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  198. package/src/room/defaults.ts +1 -0
  199. package/src/room/errors.test.ts +36 -0
  200. package/src/room/errors.ts +27 -0
  201. package/src/room/events.ts +2 -1
  202. package/src/room/participant/publishUtils.ts +0 -19
  203. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  204. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  205. package/src/room/track/LocalTrack.ts +18 -7
  206. package/src/room/track/LocalVideoTrack.ts +10 -3
  207. package/src/room/track/RemoteAudioTrack.ts +3 -3
  208. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  209. package/src/room/track/RemoteVideoTrack.ts +29 -3
  210. package/src/room/track/utils.test.ts +201 -2
  211. package/src/room/track/utils.ts +88 -0
  212. package/src/room/utils.test.ts +124 -0
  213. package/src/room/utils.ts +70 -0
  214. package/src/test/compressionStreamStubs.ts +53 -0
@@ -1,5 +1,6 @@
1
1
  import { TrackInfo, TrackPublishedResponse, TrackSource, VideoQuality } from '@livekit/protocol';
2
2
  import type { AudioProcessorOptions, TrackProcessor, VideoProcessorOptions } from '../..';
3
+ import log from '../../logger';
3
4
  import { cloneDeep } from '../../utils/cloneDeep';
4
5
  import { isSafari, sleep } from '../utils';
5
6
  import { Track } from './Track';
@@ -351,3 +352,90 @@ export function layerDimensionsFor(
351
352
  ): Track.Dimensions | undefined {
352
353
  return trackInfo.layers?.find((l) => l.quality === quality);
353
354
  }
355
+
356
+ /**
357
+ * Waits until `track` has actually produced a video frame.
358
+ *
359
+ * iOS keeps reporting the camera's sensor frame from `getSettings()` until the camera has
360
+ * delivered its first picture, so a portrait capture initially reads as landscape. The arrival
361
+ * of that first frame is the only reliable signal that the reported settings describe reality.
362
+ *
363
+ * Best effort, never throws. Resolves `true` when a frame was observed, `false` when no frame
364
+ * can be observed (the track is already ended, muted or disabled) or `timeoutMs` elapsed first.
365
+ *
366
+ * @internal
367
+ */
368
+ export async function waitForFirstVideoFrame(
369
+ track: MediaStreamTrack,
370
+ timeoutMs: number,
371
+ candidateElements: HTMLMediaElement[] = [],
372
+ ): Promise<boolean> {
373
+ if (typeof document === 'undefined') {
374
+ return false;
375
+ }
376
+ if (track.readyState === 'ended' || track.muted || !track.enabled) {
377
+ // no frame is ever going to arrive, don't hold the caller up for the full timeout
378
+ return false;
379
+ }
380
+
381
+ // requestVideoFrameCallback (Safari 15.4 and up) is a platform capability, so the answer is the
382
+ // same for every element
383
+ const supportsFrameCallback =
384
+ typeof HTMLVideoElement.prototype.requestVideoFrameCallback === 'function';
385
+
386
+ // prefer an element that is already rendering this track (a local preview, typically) over
387
+ // spinning up a second decode just to observe a frame
388
+ const existingElement = candidateElements.find(
389
+ (el): el is HTMLVideoElement =>
390
+ el instanceof HTMLVideoElement &&
391
+ el.srcObject instanceof MediaStream &&
392
+ el.srcObject.getVideoTracks().includes(track) &&
393
+ // without requestVideoFrameCallback we depend on `loadeddata`, which is one-shot: an element
394
+ // that already decoded a frame (readyState >= HAVE_CURRENT_DATA) never fires it again.
395
+ // Leave those alone and set up our own element, which starts out empty.
396
+ (supportsFrameCallback || el.readyState < 2),
397
+ );
398
+
399
+ const element = existingElement ?? document.createElement('video');
400
+ if (!existingElement) {
401
+ element.playsInline = true;
402
+ element.muted = true;
403
+ element.srcObject = new MediaStream([track]);
404
+ // the element is deliberately never added to the DOM, it only exists to pull frames.
405
+ // WebKit delivers frames to a paused element too, so a rejected play() (Safari's low power
406
+ // mode, for instance) is not a reason to stop waiting
407
+ element.play().catch((error) => {
408
+ log.debug('could not play element while waiting for the first video frame', { error });
409
+ });
410
+ }
411
+
412
+ let timer: ReturnType<typeof setTimeout> | undefined;
413
+ let frameHandle: number | undefined;
414
+ let onLoadedData: (() => void) | undefined;
415
+
416
+ try {
417
+ return await new Promise<boolean>((resolve) => {
418
+ timer = setTimeout(() => resolve(false), timeoutMs);
419
+ if (supportsFrameCallback) {
420
+ frameHandle = element.requestVideoFrameCallback(() => resolve(true));
421
+ } else {
422
+ // Safari below 15.4 has no requestVideoFrameCallback. `loadeddata` fires once the first
423
+ // frame has been decoded, which is the same signal a few milliseconds earlier.
424
+ onLoadedData = () => resolve(true);
425
+ element.addEventListener('loadeddata', onLoadedData);
426
+ }
427
+ });
428
+ } finally {
429
+ clearTimeout(timer);
430
+ if (frameHandle !== undefined) {
431
+ element.cancelVideoFrameCallback?.(frameHandle);
432
+ }
433
+ if (onLoadedData) {
434
+ element.removeEventListener('loadeddata', onLoadedData);
435
+ }
436
+ if (!existingElement) {
437
+ element.pause();
438
+ element.srcObject = null;
439
+ }
440
+ }
441
+ }
@@ -1,12 +1,22 @@
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,
7
16
  isSVCSimulcast,
8
17
  isSVCSimulcastSupportedByServer,
9
18
  negotiateDependencyDescriptor,
19
+ resetDeflateRawCompressionSupportCache,
10
20
  splitUtf8,
11
21
  supportsAdaptiveStream,
12
22
  toWebsocketUrl,
@@ -367,3 +377,117 @@ describe('usesLegacySVCEncodings', () => {
367
377
  expect(usesLegacySVCEncodings()).toBe(true);
368
378
  });
369
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
@@ -300,6 +300,33 @@ export function isSafariBased(): boolean {
300
300
  return b?.name === 'Safari' || b?.os === 'iOS';
301
301
  }
302
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
+
303
330
  export function isSafari17Based(): boolean {
304
331
  const b = getBrowser();
305
332
  return (
@@ -961,6 +988,49 @@ export function isCompressionStreamSupported() {
961
988
  return typeof CompressionStream !== 'undefined';
962
989
  }
963
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
+
964
1034
  export function isPublisherOfferWithJoinSupported() {
965
1035
  // we have connectivity issue about publisher offer with join on firefox #1919
966
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
+ }