livekit-client 2.22.3 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +71 -8
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1746 -547
- 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/api/WebSocketStream.d.ts +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.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/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +100 -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 +16 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +54 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.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/LocalParticipant.d.ts +4 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.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/rpc/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +18 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +11 -0
- package/dist/src/room/track/processor/types.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 +60 -1
- 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 +34 -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 +39 -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 +268 -175
- package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
- package/dist/ts4.2/room/RTCEngine.d.ts +277 -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 +407 -365
- package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -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 +291 -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 +62 -42
- package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
- package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -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 +42 -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 +273 -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 +394 -10
- package/src/api/SignalClient.ts +132 -76
- package/src/api/WebSocketStream.test.ts +11 -0
- package/src/api/WebSocketStream.ts +22 -1
- package/src/api/utils.ts +1 -1
- package/src/e2ee/E2eeManager.test.ts +43 -0
- package/src/e2ee/E2eeManager.ts +6 -2
- package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
- package/src/e2ee/worker/FrameCryptor.ts +21 -4
- package/src/e2ee/worker/naluUtils.test.ts +123 -0
- package/src/e2ee/worker/naluUtils.ts +57 -2
- package/src/index.ts +2 -0
- package/src/options.ts +16 -0
- package/src/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +327 -9
- package/src/room/PCTransport.ts +339 -36
- package/src/room/PCTransportManager.ts +3 -1
- package/src/room/RTCEngine.test.ts +318 -2
- package/src/room/RTCEngine.ts +126 -12
- package/src/room/Room.test.ts +148 -6
- package/src/room/Room.ts +312 -80
- package/src/room/data-channel/DataChannelManager.test.ts +24 -7
- package/src/room/data-channel/DataChannelManager.ts +43 -38
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
- package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
- package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
- package/src/room/data-channel/LossyDataChannel.ts +10 -1
- package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
- package/src/room/data-channel/ReliableDataChannel.ts +16 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +47 -20
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -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/LocalParticipant.test.ts +136 -1
- package/src/room/participant/LocalParticipant.ts +66 -17
- package/src/room/participant/Participant.ts +6 -3
- package/src/room/participant/publishUtils.ts +0 -19
- package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
- package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
- package/src/room/rpc/client/RpcClientManager.ts +32 -15
- package/src/room/rpc/utils.test.ts +32 -0
- package/src/room/rpc/utils.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +101 -10
- package/src/room/track/LocalTrack.ts +87 -19
- package/src/room/track/LocalVideoTrack.ts +17 -3
- package/src/room/track/RemoteAudioTrack.ts +59 -14
- package/src/room/track/RemoteVideoTrack.test.ts +210 -0
- package/src/room/track/RemoteVideoTrack.ts +29 -3
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +206 -2
- package/src/room/track/utils.ts +117 -5
- package/src/room/utils.test.ts +250 -0
- package/src/room/utils.ts +152 -19
- package/src/test/compressionStreamStubs.ts +53 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { audioDefaults, videoDefaults } from '../defaults';
|
|
3
3
|
import { type AudioCaptureOptions, VideoPresets } from './options';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
constraintsForOptions,
|
|
6
|
+
diffAttributes,
|
|
7
|
+
mergeDefaultOptions,
|
|
8
|
+
waitForFirstVideoFrame,
|
|
9
|
+
} from './utils';
|
|
5
10
|
|
|
6
11
|
describe('mergeDefaultOptions', () => {
|
|
7
12
|
it('does not enable undefined options', () => {
|
|
@@ -100,6 +105,11 @@ describe('constraintsForOptions', () => {
|
|
|
100
105
|
expect(videoOpts.frameRate).toEqual(VideoPresets.h720.resolution.frameRate);
|
|
101
106
|
expect(videoOpts.aspectRatio).toEqual(VideoPresets.h720.resolution.aspectRatio);
|
|
102
107
|
});
|
|
108
|
+
|
|
109
|
+
it('leaves an explicitly set exact deviceId untouched', () => {
|
|
110
|
+
const constraints = constraintsForOptions({ audio: { deviceId: { exact: 'default' } } });
|
|
111
|
+
expect((constraints.audio as MediaTrackConstraints).deviceId).toEqual({ exact: 'default' });
|
|
112
|
+
});
|
|
103
113
|
});
|
|
104
114
|
|
|
105
115
|
describe('diffAttributes', () => {
|
|
@@ -135,3 +145,197 @@ describe('diffAttributes', () => {
|
|
|
135
145
|
expect(diff.a).toBe('value');
|
|
136
146
|
});
|
|
137
147
|
});
|
|
148
|
+
|
|
149
|
+
class FakeVideoTrack extends EventTarget {
|
|
150
|
+
kind = 'video';
|
|
151
|
+
|
|
152
|
+
id = 'fake-video-track';
|
|
153
|
+
|
|
154
|
+
label = 'fake';
|
|
155
|
+
|
|
156
|
+
enabled = true;
|
|
157
|
+
|
|
158
|
+
muted = false;
|
|
159
|
+
|
|
160
|
+
readyState: MediaStreamTrackState = 'live';
|
|
161
|
+
|
|
162
|
+
settings: MediaTrackSettings;
|
|
163
|
+
|
|
164
|
+
constructor(settings: MediaTrackSettings = { width: 1280, height: 720 }) {
|
|
165
|
+
super();
|
|
166
|
+
this.settings = settings;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getSettings() {
|
|
170
|
+
return this.settings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getConstraints() {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const asMediaStreamTrack = (track: FakeVideoTrack) => track as unknown as MediaStreamTrack;
|
|
179
|
+
|
|
180
|
+
describe('waitForFirstVideoFrame', () => {
|
|
181
|
+
let frameCallbacks: Map<number, VideoFrameRequestCallback>;
|
|
182
|
+
let cancelledHandles: number[];
|
|
183
|
+
let createdVideoElements: HTMLVideoElement[];
|
|
184
|
+
|
|
185
|
+
/** happy-dom has no requestVideoFrameCallback, install a controllable one */
|
|
186
|
+
function enableFrameCallbacks() {
|
|
187
|
+
let nextHandle = 1;
|
|
188
|
+
HTMLVideoElement.prototype.requestVideoFrameCallback = function requestVideoFrameCallback(
|
|
189
|
+
callback: VideoFrameRequestCallback,
|
|
190
|
+
) {
|
|
191
|
+
const handle = nextHandle;
|
|
192
|
+
nextHandle += 1;
|
|
193
|
+
frameCallbacks.set(handle, callback);
|
|
194
|
+
return handle;
|
|
195
|
+
};
|
|
196
|
+
HTMLVideoElement.prototype.cancelVideoFrameCallback = function cancelVideoFrameCallback(
|
|
197
|
+
handle: number,
|
|
198
|
+
) {
|
|
199
|
+
cancelledHandles.push(handle);
|
|
200
|
+
frameCallbacks.delete(handle);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function deliverFrame() {
|
|
205
|
+
Array.from(frameCallbacks.values()).forEach((callback) =>
|
|
206
|
+
callback(0, {} as VideoFrameCallbackMetadata),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
frameCallbacks = new Map();
|
|
212
|
+
cancelledHandles = [];
|
|
213
|
+
createdVideoElements = [];
|
|
214
|
+
const createElement = document.createElement.bind(document);
|
|
215
|
+
vi.spyOn(document, 'createElement').mockImplementation(((
|
|
216
|
+
tagName: string,
|
|
217
|
+
options?: ElementCreationOptions,
|
|
218
|
+
) => {
|
|
219
|
+
const element = createElement(tagName, options);
|
|
220
|
+
if (tagName === 'video') {
|
|
221
|
+
createdVideoElements.push(element as HTMLVideoElement);
|
|
222
|
+
}
|
|
223
|
+
return element;
|
|
224
|
+
}) as typeof document.createElement);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
vi.restoreAllMocks();
|
|
229
|
+
// @ts-expect-error removing the stand-in we installed above
|
|
230
|
+
delete HTMLVideoElement.prototype.requestVideoFrameCallback;
|
|
231
|
+
// @ts-expect-error removing the stand-in we installed above
|
|
232
|
+
delete HTMLVideoElement.prototype.cancelVideoFrameCallback;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('resolves once a frame has been delivered', async () => {
|
|
236
|
+
enableFrameCallbacks();
|
|
237
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
|
|
238
|
+
expect(createdVideoElements).toHaveLength(1);
|
|
239
|
+
|
|
240
|
+
deliverFrame();
|
|
241
|
+
|
|
242
|
+
await expect(pending).resolves.toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('gives up once the timeout elapses', async () => {
|
|
246
|
+
enableFrameCallbacks();
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 20),
|
|
250
|
+
).resolves.toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it.each([
|
|
254
|
+
['ended', { readyState: 'ended' as MediaStreamTrackState }],
|
|
255
|
+
['muted', { muted: true }],
|
|
256
|
+
['disabled', { enabled: false }],
|
|
257
|
+
])('does not wait on a track that is %s', async (_label, overrides) => {
|
|
258
|
+
enableFrameCallbacks();
|
|
259
|
+
const track = Object.assign(new FakeVideoTrack(), overrides);
|
|
260
|
+
|
|
261
|
+
await expect(waitForFirstVideoFrame(asMediaStreamTrack(track), 10_000)).resolves.toBe(false);
|
|
262
|
+
expect(createdVideoElements).toHaveLength(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('falls back to loadeddata when requestVideoFrameCallback is unavailable', async () => {
|
|
266
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
|
|
267
|
+
|
|
268
|
+
createdVideoElements[0].dispatchEvent(new Event('loadeddata'));
|
|
269
|
+
|
|
270
|
+
await expect(pending).resolves.toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('keeps waiting when play() is rejected', async () => {
|
|
274
|
+
enableFrameCallbacks();
|
|
275
|
+
vi.spyOn(HTMLMediaElement.prototype, 'play').mockRejectedValue(new Error('NotAllowedError'));
|
|
276
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
|
|
277
|
+
|
|
278
|
+
deliverFrame();
|
|
279
|
+
|
|
280
|
+
await expect(pending).resolves.toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('reuses an element that already renders the track', async () => {
|
|
284
|
+
enableFrameCallbacks();
|
|
285
|
+
const track = new FakeVideoTrack();
|
|
286
|
+
const attached = document.createElement('video');
|
|
287
|
+
attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
|
|
288
|
+
createdVideoElements.length = 0;
|
|
289
|
+
|
|
290
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
|
|
291
|
+
expect(createdVideoElements).toHaveLength(0);
|
|
292
|
+
deliverFrame();
|
|
293
|
+
|
|
294
|
+
await expect(pending).resolves.toBe(true);
|
|
295
|
+
// the caller owns that element, it must be left rendering
|
|
296
|
+
expect(attached.srcObject).not.toBeNull();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('reuses an element that has not decoded a frame yet without requestVideoFrameCallback', async () => {
|
|
300
|
+
const track = new FakeVideoTrack();
|
|
301
|
+
const attached = document.createElement('video');
|
|
302
|
+
attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
|
|
303
|
+
Object.defineProperty(attached, 'readyState', { value: 1, configurable: true });
|
|
304
|
+
createdVideoElements.length = 0;
|
|
305
|
+
|
|
306
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
|
|
307
|
+
expect(createdVideoElements).toHaveLength(0);
|
|
308
|
+
// `loadeddata` has not fired on that element yet, so listening for it still works
|
|
309
|
+
attached.dispatchEvent(new Event('loadeddata'));
|
|
310
|
+
|
|
311
|
+
await expect(pending).resolves.toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('declines an already loaded element without requestVideoFrameCallback', async () => {
|
|
315
|
+
const track = new FakeVideoTrack();
|
|
316
|
+
const attached = document.createElement('video');
|
|
317
|
+
attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
|
|
318
|
+
Object.defineProperty(attached, 'readyState', { value: 2, configurable: true });
|
|
319
|
+
createdVideoElements.length = 0;
|
|
320
|
+
|
|
321
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
|
|
322
|
+
// `loadeddata` already fired on that element and is never replayed, so we need our own
|
|
323
|
+
expect(createdVideoElements).toHaveLength(1);
|
|
324
|
+
createdVideoElements[0].dispatchEvent(new Event('loadeddata'));
|
|
325
|
+
|
|
326
|
+
await expect(pending).resolves.toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('tears down the element it created', async () => {
|
|
330
|
+
enableFrameCallbacks();
|
|
331
|
+
const pause = vi.spyOn(HTMLMediaElement.prototype, 'pause');
|
|
332
|
+
const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
|
|
333
|
+
|
|
334
|
+
deliverFrame();
|
|
335
|
+
await pending;
|
|
336
|
+
|
|
337
|
+
expect(createdVideoElements[0].srcObject).toBeNull();
|
|
338
|
+
expect(pause).toHaveBeenCalled();
|
|
339
|
+
expect(cancelledHandles).toHaveLength(1);
|
|
340
|
+
});
|
|
341
|
+
});
|
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';
|
|
@@ -107,22 +108,46 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
|
|
|
107
108
|
*/
|
|
108
109
|
export async function detectSilence(track: AudioTrack, timeOffset = 200): Promise<boolean> {
|
|
109
110
|
const ctx = getNewAudioContext();
|
|
110
|
-
if (ctx) {
|
|
111
|
-
|
|
111
|
+
if (!ctx) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
let source: MediaStreamAudioSourceNode | undefined;
|
|
115
|
+
let analyser: AnalyserNode | undefined;
|
|
116
|
+
try {
|
|
117
|
+
analyser = ctx.createAnalyser();
|
|
112
118
|
analyser.fftSize = 2048;
|
|
113
119
|
|
|
114
120
|
const bufferLength = analyser.frequencyBinCount;
|
|
115
121
|
const dataArray = new Uint8Array(bufferLength);
|
|
116
|
-
|
|
122
|
+
source = ctx.createMediaStreamSource(new MediaStream([track.mediaStreamTrack]));
|
|
117
123
|
|
|
118
124
|
source.connect(analyser);
|
|
125
|
+
// read through a function so that `state` isn't narrowed to its value from before `resume`
|
|
126
|
+
const isRunning = () => ctx.state === 'running';
|
|
127
|
+
if (!isRunning()) {
|
|
128
|
+
try {
|
|
129
|
+
// `resume` can stay pending indefinitely on a blocked page rather than rejecting, which
|
|
130
|
+
// would keep the context and its nodes alive past the `finally` below
|
|
131
|
+
await Promise.race([ctx.resume(), sleep(200)]);
|
|
132
|
+
} catch {
|
|
133
|
+
// autoplay policies can keep us from resuming the context
|
|
134
|
+
}
|
|
135
|
+
if (!isRunning()) {
|
|
136
|
+
// a context that isn't running doesn't process any audio, so the analyser would only ever
|
|
137
|
+
// read silence. Report the track as not silent rather than raising a false positive.
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
await sleep(timeOffset);
|
|
120
142
|
analyser.getByteTimeDomainData(dataArray);
|
|
121
143
|
const someNoise = dataArray.some((sample) => sample !== 128 && sample !== 0);
|
|
122
|
-
ctx.close();
|
|
123
144
|
return !someNoise;
|
|
145
|
+
} finally {
|
|
146
|
+
source?.disconnect();
|
|
147
|
+
analyser?.disconnect();
|
|
148
|
+
// don't let a failing teardown mask the detection result
|
|
149
|
+
await ctx.close().catch(() => {});
|
|
124
150
|
}
|
|
125
|
-
return false;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
/**
|
|
@@ -351,3 +376,90 @@ export function layerDimensionsFor(
|
|
|
351
376
|
): Track.Dimensions | undefined {
|
|
352
377
|
return trackInfo.layers?.find((l) => l.quality === quality);
|
|
353
378
|
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Waits until `track` has actually produced a video frame.
|
|
382
|
+
*
|
|
383
|
+
* iOS keeps reporting the camera's sensor frame from `getSettings()` until the camera has
|
|
384
|
+
* delivered its first picture, so a portrait capture initially reads as landscape. The arrival
|
|
385
|
+
* of that first frame is the only reliable signal that the reported settings describe reality.
|
|
386
|
+
*
|
|
387
|
+
* Best effort, never throws. Resolves `true` when a frame was observed, `false` when no frame
|
|
388
|
+
* can be observed (the track is already ended, muted or disabled) or `timeoutMs` elapsed first.
|
|
389
|
+
*
|
|
390
|
+
* @internal
|
|
391
|
+
*/
|
|
392
|
+
export async function waitForFirstVideoFrame(
|
|
393
|
+
track: MediaStreamTrack,
|
|
394
|
+
timeoutMs: number,
|
|
395
|
+
candidateElements: HTMLMediaElement[] = [],
|
|
396
|
+
): Promise<boolean> {
|
|
397
|
+
if (typeof document === 'undefined') {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (track.readyState === 'ended' || track.muted || !track.enabled) {
|
|
401
|
+
// no frame is ever going to arrive, don't hold the caller up for the full timeout
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// requestVideoFrameCallback (Safari 15.4 and up) is a platform capability, so the answer is the
|
|
406
|
+
// same for every element
|
|
407
|
+
const supportsFrameCallback =
|
|
408
|
+
typeof HTMLVideoElement.prototype.requestVideoFrameCallback === 'function';
|
|
409
|
+
|
|
410
|
+
// prefer an element that is already rendering this track (a local preview, typically) over
|
|
411
|
+
// spinning up a second decode just to observe a frame
|
|
412
|
+
const existingElement = candidateElements.find(
|
|
413
|
+
(el): el is HTMLVideoElement =>
|
|
414
|
+
el instanceof HTMLVideoElement &&
|
|
415
|
+
el.srcObject instanceof MediaStream &&
|
|
416
|
+
el.srcObject.getVideoTracks().includes(track) &&
|
|
417
|
+
// without requestVideoFrameCallback we depend on `loadeddata`, which is one-shot: an element
|
|
418
|
+
// that already decoded a frame (readyState >= HAVE_CURRENT_DATA) never fires it again.
|
|
419
|
+
// Leave those alone and set up our own element, which starts out empty.
|
|
420
|
+
(supportsFrameCallback || el.readyState < 2),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const element = existingElement ?? document.createElement('video');
|
|
424
|
+
if (!existingElement) {
|
|
425
|
+
element.playsInline = true;
|
|
426
|
+
element.muted = true;
|
|
427
|
+
element.srcObject = new MediaStream([track]);
|
|
428
|
+
// the element is deliberately never added to the DOM, it only exists to pull frames.
|
|
429
|
+
// WebKit delivers frames to a paused element too, so a rejected play() (Safari's low power
|
|
430
|
+
// mode, for instance) is not a reason to stop waiting
|
|
431
|
+
element.play().catch((error) => {
|
|
432
|
+
log.debug('could not play element while waiting for the first video frame', { error });
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
437
|
+
let frameHandle: number | undefined;
|
|
438
|
+
let onLoadedData: (() => void) | undefined;
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
return await new Promise<boolean>((resolve) => {
|
|
442
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
443
|
+
if (supportsFrameCallback) {
|
|
444
|
+
frameHandle = element.requestVideoFrameCallback(() => resolve(true));
|
|
445
|
+
} else {
|
|
446
|
+
// Safari below 15.4 has no requestVideoFrameCallback. `loadeddata` fires once the first
|
|
447
|
+
// frame has been decoded, which is the same signal a few milliseconds earlier.
|
|
448
|
+
onLoadedData = () => resolve(true);
|
|
449
|
+
element.addEventListener('loadeddata', onLoadedData);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
} finally {
|
|
453
|
+
clearTimeout(timer);
|
|
454
|
+
if (frameHandle !== undefined) {
|
|
455
|
+
element.cancelVideoFrameCallback?.(frameHandle);
|
|
456
|
+
}
|
|
457
|
+
if (onLoadedData) {
|
|
458
|
+
element.removeEventListener('loadeddata', onLoadedData);
|
|
459
|
+
}
|
|
460
|
+
if (!existingElement) {
|
|
461
|
+
element.pause();
|
|
462
|
+
element.srcObject = null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
package/src/room/utils.test.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
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
|
+
isPermissionDeniedError,
|
|
7
17
|
isSVCSimulcast,
|
|
8
18
|
isSVCSimulcastSupportedByServer,
|
|
9
19
|
negotiateDependencyDescriptor,
|
|
20
|
+
resetDeflateRawCompressionSupportCache,
|
|
10
21
|
splitUtf8,
|
|
11
22
|
supportsAdaptiveStream,
|
|
12
23
|
toWebsocketUrl,
|
|
@@ -367,3 +378,242 @@ describe('usesLegacySVCEncodings', () => {
|
|
|
367
378
|
expect(usesLegacySVCEncodings()).toBe(true);
|
|
368
379
|
});
|
|
369
380
|
});
|
|
381
|
+
|
|
382
|
+
describe('isIPadOS / isAppleMobile', () => {
|
|
383
|
+
const stub = (userAgent: string, maxTouchPoints: number) =>
|
|
384
|
+
vi.stubGlobal('navigator', { userAgent, maxTouchPoints, product: 'Gecko' });
|
|
385
|
+
|
|
386
|
+
afterEach(() => {
|
|
387
|
+
vi.unstubAllGlobals();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// measured on an iPad 6 running iPadOS 17: the default (desktop) user agent, maxTouchPoints 5
|
|
391
|
+
const IPAD_DESKTOP_UA =
|
|
392
|
+
'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';
|
|
393
|
+
// measured on macOS Safari 26.4: the same shape of user agent, maxTouchPoints 0
|
|
394
|
+
const MAC_SAFARI_UA =
|
|
395
|
+
'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';
|
|
396
|
+
const IPHONE_UA =
|
|
397
|
+
'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';
|
|
398
|
+
const IPAD_MOBILE_UA =
|
|
399
|
+
'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';
|
|
400
|
+
const MAC_CHROME_UA =
|
|
401
|
+
'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';
|
|
402
|
+
const ANDROID_CHROME_UA =
|
|
403
|
+
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36';
|
|
404
|
+
|
|
405
|
+
it('detects an iPad sending the desktop user agent', () => {
|
|
406
|
+
stub(IPAD_DESKTOP_UA, 5);
|
|
407
|
+
expect(isIPadOS()).toBe(true);
|
|
408
|
+
expect(isAppleMobile()).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('is the case getBrowser() alone gets wrong', () => {
|
|
412
|
+
// the gap this exists to close: iPadOS parses as macOS, so `os === 'iOS'` misses it
|
|
413
|
+
stub(IPAD_DESKTOP_UA, 5);
|
|
414
|
+
expect(getBrowser()?.os).toBe('macOS');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('leaves macOS Safari alone, where the camera reports correctly and frames are slow', () => {
|
|
418
|
+
stub(MAC_SAFARI_UA, 0);
|
|
419
|
+
expect(isIPadOS()).toBe(false);
|
|
420
|
+
expect(isAppleMobile()).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('covers iPhones through the existing iOS check', () => {
|
|
424
|
+
stub(IPHONE_UA, 5);
|
|
425
|
+
expect(isIPadOS()).toBe(false);
|
|
426
|
+
expect(isAppleMobile()).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('covers an iPad set to request the mobile site', () => {
|
|
430
|
+
stub(IPAD_MOBILE_UA, 5);
|
|
431
|
+
expect(isIPadOS()).toBe(false);
|
|
432
|
+
expect(isAppleMobile()).toBe(true);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('does not fire for a Mac with a touchscreen attached, which is not Safari', () => {
|
|
436
|
+
stub(MAC_CHROME_UA, 5);
|
|
437
|
+
expect(isIPadOS()).toBe(false);
|
|
438
|
+
expect(isAppleMobile()).toBe(false);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('does not fire on android', () => {
|
|
442
|
+
stub(ANDROID_CHROME_UA, 5);
|
|
443
|
+
expect(isIPadOS()).toBe(false);
|
|
444
|
+
expect(isAppleMobile()).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('does not fire on a desktop safari reporting no touch support', () => {
|
|
448
|
+
stub(MAC_SAFARI_UA, 1);
|
|
449
|
+
expect(isIPadOS()).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('isDeflateRawCompressionSupported', () => {
|
|
454
|
+
afterEach(() => {
|
|
455
|
+
resetDeflateRawCompressionSupportCache();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('is true in a runtime that supports the deflate-raw format', () => {
|
|
459
|
+
expect(isDeflateRawCompressionSupported()).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('is false without the compression streams api at all', () => {
|
|
463
|
+
const restoreCompressionStreams = stubMissingCompressionStreams();
|
|
464
|
+
try {
|
|
465
|
+
expect(isDeflateRawCompressionSupported()).toBe(false);
|
|
466
|
+
expect(isCompressionStreamSupported()).toBe(false);
|
|
467
|
+
} finally {
|
|
468
|
+
restoreCompressionStreams();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('is false when the api exists but deflate-raw is not a supported format', () => {
|
|
473
|
+
const restoreCompressionStreams = stubMissingDeflateRawSupport();
|
|
474
|
+
try {
|
|
475
|
+
expect(isDeflateRawCompressionSupported()).toBe(false);
|
|
476
|
+
// Chromium 80-102 still compresses gzip, which is all the wrapped join request needs.
|
|
477
|
+
expect(isCompressionStreamSupported()).toBe(true);
|
|
478
|
+
} finally {
|
|
479
|
+
restoreCompressionStreams();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('probes the runtime only once', () => {
|
|
484
|
+
expect(isDeflateRawCompressionSupported()).toBe(true);
|
|
485
|
+
|
|
486
|
+
const originalCompressionStream = globalThis.CompressionStream;
|
|
487
|
+
try {
|
|
488
|
+
(globalThis as any).CompressionStream = undefined;
|
|
489
|
+
expect(isDeflateRawCompressionSupported()).toBe(true);
|
|
490
|
+
} finally {
|
|
491
|
+
(globalThis as any).CompressionStream = originalCompressionStream;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('isPermissionDeniedError', () => {
|
|
497
|
+
function errorNamed(name: string) {
|
|
498
|
+
const e = new Error(name);
|
|
499
|
+
e.name = name;
|
|
500
|
+
return e;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
it('matches the permission denial names and their legacy aliases', () => {
|
|
504
|
+
expect(isPermissionDeniedError(errorNamed('NotAllowedError'))).toBe(true);
|
|
505
|
+
expect(isPermissionDeniedError(errorNamed('SecurityError'))).toBe(true);
|
|
506
|
+
expect(isPermissionDeniedError(errorNamed('PermissionDeniedError'))).toBe(true);
|
|
507
|
+
expect(isPermissionDeniedError(errorNamed('PermissionDismissedError'))).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('does not match failures that a softer constraint could still satisfy', () => {
|
|
511
|
+
expect(isPermissionDeniedError(errorNamed('OverconstrainedError'))).toBe(false);
|
|
512
|
+
expect(isPermissionDeniedError(errorNamed('ConstraintNotSatisfiedError'))).toBe(false);
|
|
513
|
+
expect(isPermissionDeniedError(errorNamed('NotFoundError'))).toBe(false);
|
|
514
|
+
expect(isPermissionDeniedError(errorNamed('SomeNameNobodyHasShippedYet'))).toBe(false);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('does not match a plain error, undefined or null', () => {
|
|
518
|
+
expect(isPermissionDeniedError(new Error('boom'))).toBe(false);
|
|
519
|
+
expect(isPermissionDeniedError(undefined)).toBe(false);
|
|
520
|
+
expect(isPermissionDeniedError(null)).toBe(false);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// `freshUtils` below calls `vi.resetModules()`, so a dynamic import of `./utils` from a later
|
|
525
|
+
// block would get a different module instance than the static import at the top of this file.
|
|
526
|
+
// Keep this block last.
|
|
527
|
+
describe('empty audio stream track refcounting', () => {
|
|
528
|
+
let closedContexts: number;
|
|
529
|
+
|
|
530
|
+
function makeTrack(): MediaStreamTrack {
|
|
531
|
+
const track = {
|
|
532
|
+
enabled: true,
|
|
533
|
+
stopped: false,
|
|
534
|
+
stop() {
|
|
535
|
+
track.stopped = true;
|
|
536
|
+
},
|
|
537
|
+
clone: () => makeTrack(),
|
|
538
|
+
};
|
|
539
|
+
return track as unknown as MediaStreamTrack;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** reloads `./utils` so its module level context and refcount start empty */
|
|
543
|
+
async function freshUtils() {
|
|
544
|
+
closedContexts = 0;
|
|
545
|
+
vi.resetModules();
|
|
546
|
+
vi.stubGlobal(
|
|
547
|
+
'AudioContext',
|
|
548
|
+
class {
|
|
549
|
+
createOscillator() {
|
|
550
|
+
return { connect: () => {}, start: () => {} };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
createGain() {
|
|
554
|
+
return { gain: { setValueAtTime: () => {} }, connect: () => {} };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
createMediaStreamDestination() {
|
|
558
|
+
return { stream: { getAudioTracks: () => [makeTrack()] } };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async close() {
|
|
562
|
+
closedContexts += 1;
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
return import('./utils');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
afterEach(() => {
|
|
570
|
+
vi.unstubAllGlobals();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('closes the shared context once every track handed out is released', async () => {
|
|
574
|
+
const { getEmptyAudioStreamTrack, releaseEmptyAudioStreamTrack } = await freshUtils();
|
|
575
|
+
const first = getEmptyAudioStreamTrack();
|
|
576
|
+
const second = getEmptyAudioStreamTrack();
|
|
577
|
+
|
|
578
|
+
await releaseEmptyAudioStreamTrack(first);
|
|
579
|
+
expect(closedContexts).toBe(0);
|
|
580
|
+
|
|
581
|
+
await releaseEmptyAudioStreamTrack(second);
|
|
582
|
+
expect(closedContexts).toBe(1);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('ignores a second release of the same track', async () => {
|
|
586
|
+
const { getEmptyAudioStreamTrack, releaseEmptyAudioStreamTrack } = await freshUtils();
|
|
587
|
+
const first = getEmptyAudioStreamTrack();
|
|
588
|
+
const second = getEmptyAudioStreamTrack();
|
|
589
|
+
|
|
590
|
+
await releaseEmptyAudioStreamTrack(first);
|
|
591
|
+
await releaseEmptyAudioStreamTrack(first);
|
|
592
|
+
expect(closedContexts).toBe(0);
|
|
593
|
+
|
|
594
|
+
await releaseEmptyAudioStreamTrack(second);
|
|
595
|
+
expect(closedContexts).toBe(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('leaves a track it never handed out alone', async () => {
|
|
599
|
+
const { getEmptyAudioStreamTrack, releaseEmptyAudioStreamTrack } = await freshUtils();
|
|
600
|
+
const owned = getEmptyAudioStreamTrack();
|
|
601
|
+
const foreign = makeTrack();
|
|
602
|
+
|
|
603
|
+
await releaseEmptyAudioStreamTrack(foreign);
|
|
604
|
+
expect(closedContexts).toBe(0);
|
|
605
|
+
expect((foreign as unknown as { stopped: boolean }).stopped).toBe(false);
|
|
606
|
+
|
|
607
|
+
await releaseEmptyAudioStreamTrack(owned);
|
|
608
|
+
expect(closedContexts).toBe(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('builds a fresh context for an acquire that arrives after the last release', async () => {
|
|
612
|
+
const { getEmptyAudioStreamTrack, releaseEmptyAudioStreamTrack } = await freshUtils();
|
|
613
|
+
await releaseEmptyAudioStreamTrack(getEmptyAudioStreamTrack());
|
|
614
|
+
expect(closedContexts).toBe(1);
|
|
615
|
+
|
|
616
|
+
await releaseEmptyAudioStreamTrack(getEmptyAudioStreamTrack());
|
|
617
|
+
expect(closedContexts).toBe(2);
|
|
618
|
+
});
|
|
619
|
+
});
|