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.
- package/README.md +35 -0
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +2 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +602 -247
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +2 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +4 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/options.d.ts +14 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +78 -7
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +18 -0
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +3 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +2 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +13 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +32 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/compressionStreamStubs.d.ts +12 -0
- package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +187 -185
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
- package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
- package/dist/ts4.2/api/utils.d.ts +9 -9
- package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
- package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
- package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
- package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
- package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
- package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
- package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
- package/dist/ts4.2/e2ee/constants.d.ts +21 -21
- package/dist/ts4.2/e2ee/errors.d.ts +12 -12
- package/dist/ts4.2/e2ee/events.d.ts +39 -39
- package/dist/ts4.2/e2ee/index.d.ts +6 -6
- package/dist/ts4.2/e2ee/types.d.ts +231 -231
- package/dist/ts4.2/e2ee/utils.d.ts +32 -32
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
- package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
- package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
- package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
- package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
- package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
- package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
- package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
- package/dist/ts4.2/index.d.ts +76 -76
- package/dist/ts4.2/logger.d.ts +66 -66
- package/dist/ts4.2/options.d.ts +163 -149
- package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
- package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
- package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
- package/dist/ts4.2/room/PCTransport.d.ts +246 -175
- package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
- package/dist/ts4.2/room/RTCEngine.d.ts +269 -262
- package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
- package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
- package/dist/ts4.2/room/Room.d.ts +354 -365
- package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
- package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
- package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
- package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
- package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
- package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
- package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
- package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
- package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
- package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
- package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
- package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
- package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
- package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
- package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
- package/dist/ts4.2/room/data-track/types.d.ts +22 -22
- package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
- package/dist/ts4.2/room/debounce.d.ts +11 -11
- package/dist/ts4.2/room/defaults.d.ts +9 -9
- package/dist/ts4.2/room/errors.d.ts +180 -162
- package/dist/ts4.2/room/events.d.ts +638 -637
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
- package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
- package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
- package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/index.d.ts +7 -7
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
- package/dist/ts4.2/room/stats.d.ts +71 -71
- package/dist/ts4.2/room/statsSummary.d.ts +13 -13
- package/dist/ts4.2/room/timers.d.ts +12 -12
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
- package/dist/ts4.2/room/token-source/types.d.ts +70 -70
- package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
- package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
- package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
- package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -100
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
- package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
- package/dist/ts4.2/room/track/Track.d.ts +156 -166
- package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
- package/dist/ts4.2/room/track/create.d.ts +25 -25
- package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
- package/dist/ts4.2/room/track/options.d.ts +403 -403
- package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
- package/dist/ts4.2/room/track/record.d.ts +13 -13
- package/dist/ts4.2/room/track/types.d.ts +28 -28
- package/dist/ts4.2/room/track/utils.d.ts +60 -47
- package/dist/ts4.2/room/types.d.ts +127 -127
- package/dist/ts4.2/room/utils.d.ts +246 -214
- package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
- package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
- package/dist/ts4.2/test/mocks.d.ts +12 -12
- package/dist/ts4.2/test/promiseState.d.ts +12 -12
- package/dist/ts4.2/test/signalToken.d.ts +26 -26
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
- package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
- package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
- package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
- package/dist/ts4.2/utils/browserParser.d.ts +13 -13
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
- package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
- package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
- package/dist/ts4.2/utils/serializer.d.ts +48 -48
- package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
- package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
- package/dist/ts4.2/version.d.ts +15 -15
- package/package.json +5 -4
- package/src/api/SignalClient.e2e.test.ts +1 -3
- package/src/api/SignalClient.test.ts +160 -10
- package/src/api/SignalClient.ts +75 -54
- package/src/options.ts +16 -0
- package/src/room/PCTransport.test.ts +183 -7
- package/src/room/PCTransport.ts +197 -36
- package/src/room/PCTransportManager.ts +3 -1
- package/src/room/RTCEngine.test.ts +84 -0
- package/src/room/RTCEngine.ts +24 -5
- package/src/room/Room.test.ts +32 -6
- package/src/room/Room.ts +26 -6
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.test.ts +36 -0
- package/src/room/errors.ts +27 -0
- package/src/room/events.ts +2 -1
- package/src/room/participant/publishUtils.ts +0 -19
- package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
- package/src/room/rpc/client/RpcClientManager.ts +32 -15
- package/src/room/track/LocalTrack.ts +18 -7
- package/src/room/track/LocalVideoTrack.ts +10 -3
- package/src/room/track/RemoteAudioTrack.ts +3 -3
- package/src/room/track/RemoteVideoTrack.test.ts +210 -0
- package/src/room/track/RemoteVideoTrack.ts +29 -3
- package/src/room/track/utils.test.ts +201 -2
- package/src/room/track/utils.ts +88 -0
- package/src/room/utils.test.ts +124 -0
- package/src/room/utils.ts +70 -0
- package/src/test/compressionStreamStubs.ts +53 -0
package/src/room/track/utils.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/room/utils.test.ts
CHANGED
|
@@ -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
|
+
}
|