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
|
@@ -5,12 +5,20 @@ import { debounce } from '../debounce';
|
|
|
5
5
|
import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
|
|
6
6
|
import { TrackEvent } from '../events';
|
|
7
7
|
import type { LoggerOptions } from '../types';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
compareVersions,
|
|
10
|
+
isAppleMobile,
|
|
11
|
+
isMobile,
|
|
12
|
+
markDeviceAcquisitionFailure,
|
|
13
|
+
sleep,
|
|
14
|
+
unwrapConstraint,
|
|
15
|
+
} from '../utils';
|
|
9
16
|
import { Track, attachToElement, detachTrack } from './Track';
|
|
10
17
|
import type { VideoCodec } from './options';
|
|
11
18
|
import type { TrackProcessor } from './processor/types';
|
|
12
19
|
import { LocalTrackRecorder, isRecordingSupported } from './record';
|
|
13
20
|
import type { ReplaceTrackOptions } from './types';
|
|
21
|
+
import { waitForFirstVideoFrame } from './utils';
|
|
14
22
|
|
|
15
23
|
const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
|
|
16
24
|
const PRE_CONNECT_BUFFER_TIMEOUT = 10_000;
|
|
@@ -131,6 +139,10 @@ export default abstract class LocalTrack<
|
|
|
131
139
|
return this.providedByUser;
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
get isStopped() {
|
|
143
|
+
return this.manuallyStopped;
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
get mediaStreamTrack() {
|
|
135
147
|
return this.processor?.processedTrack ?? this._mediaStreamTrack;
|
|
136
148
|
}
|
|
@@ -194,6 +206,9 @@ export default abstract class LocalTrack<
|
|
|
194
206
|
track: newTrack,
|
|
195
207
|
kind: this.kind,
|
|
196
208
|
element: this.processorElement,
|
|
209
|
+
// audio processors need the context they were initialised with in order to rebuild
|
|
210
|
+
// their graph, omitting it here would leave them without one
|
|
211
|
+
audioContext: this.audioContext,
|
|
197
212
|
localTrack: this,
|
|
198
213
|
});
|
|
199
214
|
processedTrack = this.processor.processedTrack;
|
|
@@ -217,6 +232,17 @@ export default abstract class LocalTrack<
|
|
|
217
232
|
attachToElement(processedTrack ?? newTrack, el);
|
|
218
233
|
});
|
|
219
234
|
}
|
|
235
|
+
await this.onMediaStreamTrackChanged();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Hook invoked after the underlying MediaStreamTrack has been swapped and the processor has
|
|
240
|
+
* been restarted around it, so `processor.processedTrack` already holds the new track.
|
|
241
|
+
* Unlike `onSenderTrackSwapped` this also fires on the restart and unmute paths.
|
|
242
|
+
*/
|
|
243
|
+
protected async onMediaStreamTrackChanged(): Promise<void> {
|
|
244
|
+
// base implementation is a no-op; LocalAudioTrack overrides this to move the krisp
|
|
245
|
+
// feature listeners onto the new processed track.
|
|
220
246
|
}
|
|
221
247
|
|
|
222
248
|
async waitForDimensions(timeout = DEFAULT_DIMENSIONS_TIMEOUT): Promise<Track.Dimensions> {
|
|
@@ -224,18 +250,28 @@ export default abstract class LocalTrack<
|
|
|
224
250
|
throw new Error('cannot get dimensions for audio tracks');
|
|
225
251
|
}
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
253
|
+
const started = Date.now();
|
|
254
|
+
|
|
255
|
+
if (isAppleMobile()) {
|
|
256
|
+
// iOS and iPadOS keep reporting the camera's sensor frame from getSettings() until the
|
|
257
|
+
// camera has delivered its first picture, so a portrait capture initially reads as
|
|
258
|
+
// landscape. Wait for that first frame instead of guessing at a delay.
|
|
259
|
+
// Gated to the Apple capture pipeline rather than run everywhere: elsewhere getSettings() is
|
|
260
|
+
// already right and the first frame can be hundreds of milliseconds out (measured ~500ms on
|
|
261
|
+
// macOS Safari), which would be pure added publish latency.
|
|
262
|
+
// https://github.com/livekit/client-sdk-js/issues/2099
|
|
263
|
+
// https://github.com/livekit/client-sdk-js/issues/2107
|
|
264
|
+
await waitForFirstVideoFrame(this._mediaStreamTrack, timeout, this.attachedElements);
|
|
231
265
|
}
|
|
232
266
|
|
|
233
|
-
|
|
234
|
-
while (Date.now() - started < timeout) {
|
|
267
|
+
while (true) {
|
|
235
268
|
const dims = this.dimensions;
|
|
236
269
|
if (dims) {
|
|
237
270
|
return dims;
|
|
238
271
|
}
|
|
272
|
+
if (Date.now() - started >= timeout) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
239
275
|
await sleep(50);
|
|
240
276
|
}
|
|
241
277
|
throw new TrackInvalidError('unable to get track dimensions after timeout');
|
|
@@ -244,7 +280,8 @@ export default abstract class LocalTrack<
|
|
|
244
280
|
async setDeviceId(deviceId: ConstrainDOMString): Promise<boolean> {
|
|
245
281
|
if (
|
|
246
282
|
this._constraints.deviceId === deviceId &&
|
|
247
|
-
this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)
|
|
283
|
+
this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId) &&
|
|
284
|
+
this._mediaStreamTrack.readyState === 'live'
|
|
248
285
|
) {
|
|
249
286
|
return true;
|
|
250
287
|
}
|
|
@@ -375,19 +412,37 @@ export default abstract class LocalTrack<
|
|
|
375
412
|
this._mediaStreamTrack.stop();
|
|
376
413
|
|
|
377
414
|
// create new track and attach
|
|
378
|
-
|
|
415
|
+
let mediaStream: MediaStream;
|
|
416
|
+
try {
|
|
417
|
+
mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
markDeviceAcquisitionFailure(e);
|
|
420
|
+
throw e;
|
|
421
|
+
}
|
|
379
422
|
const newTrack = mediaStream.getTracks()[0];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
423
|
+
try {
|
|
424
|
+
if (this.kind === Track.Kind.Video) {
|
|
425
|
+
// we already captured the audio track with the constraints, so we only need to apply the video constraints
|
|
426
|
+
await newTrack.applyConstraints(otherConstraints);
|
|
427
|
+
}
|
|
428
|
+
newTrack.addEventListener('ended', this.handleEnded);
|
|
429
|
+
this.log.debug('re-acquired MediaStreamTrack', this.logContext);
|
|
430
|
+
|
|
431
|
+
await this.setMediaStreamTrack(newTrack, false, isUnmuting);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
// setMediaStreamTrack can throw after acquisition but before it adopts the new track,
|
|
434
|
+
// which would leave the capture device live with nothing referencing it
|
|
435
|
+
if (this._mediaStreamTrack !== newTrack) {
|
|
436
|
+
newTrack.stop();
|
|
437
|
+
}
|
|
438
|
+
throw e;
|
|
383
439
|
}
|
|
384
|
-
newTrack.addEventListener('ended', this.handleEnded);
|
|
385
|
-
this.log.debug('re-acquired MediaStreamTrack', this.logContext);
|
|
386
|
-
|
|
387
|
-
await this.setMediaStreamTrack(newTrack, false, isUnmuting);
|
|
388
440
|
this._constraints = constraints;
|
|
389
441
|
this.pendingDeviceChange = false;
|
|
390
442
|
this.emit(TrackEvent.Restarted, this);
|
|
443
|
+
return this;
|
|
444
|
+
} finally {
|
|
445
|
+
// in the finally because a throw can also leave the new track adopted, and so still live
|
|
391
446
|
if (this.manuallyStopped) {
|
|
392
447
|
this.log.warn(
|
|
393
448
|
'track was stopped during a restart, stopping restarted track',
|
|
@@ -395,8 +450,6 @@ export default abstract class LocalTrack<
|
|
|
395
450
|
);
|
|
396
451
|
this.stop();
|
|
397
452
|
}
|
|
398
|
-
return this;
|
|
399
|
-
} finally {
|
|
400
453
|
unlock();
|
|
401
454
|
}
|
|
402
455
|
}
|
|
@@ -464,7 +517,11 @@ export default abstract class LocalTrack<
|
|
|
464
517
|
this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
|
|
465
518
|
this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
|
|
466
519
|
this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
|
|
467
|
-
|
|
520
|
+
// `stop` is synchronous, so we can't await the teardown of the processor here.
|
|
521
|
+
// make sure a failing teardown doesn't surface as an unhandled rejection.
|
|
522
|
+
this.processor?.destroy().catch((error) => {
|
|
523
|
+
this.log.error('failed to destroy processor', { ...this.logContext, error });
|
|
524
|
+
});
|
|
468
525
|
this.processor = undefined;
|
|
469
526
|
}
|
|
470
527
|
|
|
@@ -540,6 +597,11 @@ export default abstract class LocalTrack<
|
|
|
540
597
|
* Sets a processor on this track.
|
|
541
598
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
542
599
|
*
|
|
600
|
+
* The processor lives on the track, so stopping the track destroys it. Unpublishing and
|
|
601
|
+
* disconnecting both stop the track by default, which means a processor has to be set again on
|
|
602
|
+
* the track published after a reconnect. See {@link TrackProcessor} for the full lifecycle,
|
|
603
|
+
* including what `stopLocalTrackOnUnpublish: false` changes.
|
|
604
|
+
*
|
|
543
605
|
* @param processor
|
|
544
606
|
* @param showProcessedStreamLocally
|
|
545
607
|
* @returns
|
|
@@ -606,6 +668,10 @@ export default abstract class LocalTrack<
|
|
|
606
668
|
await this.onSenderTrackSwapped();
|
|
607
669
|
}
|
|
608
670
|
|
|
671
|
+
/**
|
|
672
|
+
* Returns the processor currently set on this track, or `undefined` if there is none. A track
|
|
673
|
+
* that was stopped by an unpublish or a disconnect has none. See {@link TrackProcessor}.
|
|
674
|
+
*/
|
|
609
675
|
getProcessor() {
|
|
610
676
|
return this.processor;
|
|
611
677
|
}
|
|
@@ -614,6 +680,8 @@ export default abstract class LocalTrack<
|
|
|
614
680
|
* Stops the track processor
|
|
615
681
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
616
682
|
*
|
|
683
|
+
* Only needed to remove a processor from a track that stays published. Unpublishing or
|
|
684
|
+
* disconnecting already destroys it. See {@link TrackProcessor}.
|
|
617
685
|
*/
|
|
618
686
|
async stopProcessor(keepElement = true) {
|
|
619
687
|
const unlock = await this.trackChangeLock.lock();
|
|
@@ -401,6 +401,13 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
|
401
401
|
await sender.setParameters(params);
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Sets a processor on this track, and wires its processed track into the simulcast codecs.
|
|
406
|
+
*
|
|
407
|
+
* The processor is destroyed when the track is stopped, which unpublishing and disconnecting
|
|
408
|
+
* both do by default, so it has to be set again after reconnecting. See {@link TrackProcessor}
|
|
409
|
+
* for the full lifecycle.
|
|
410
|
+
*/
|
|
404
411
|
async setProcessor(
|
|
405
412
|
processor: TrackProcessor<Track.Kind.Video>,
|
|
406
413
|
showProcessedStreamLocally = true,
|
|
@@ -482,12 +489,19 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
|
482
489
|
// browser will reenable disabled codec/layers after new codec has been published,
|
|
483
490
|
// so refresh subscribedCodecs after publish a new codec
|
|
484
491
|
setTimeout(() => {
|
|
485
|
-
|
|
486
|
-
this.setPublishingCodecs(this.subscribedCodecs);
|
|
487
|
-
}
|
|
492
|
+
this.refreshSubscribedCodecs();
|
|
488
493
|
}, refreshSubscribedCodecAfterNewCodec);
|
|
489
494
|
}
|
|
490
495
|
|
|
496
|
+
/** @interal */
|
|
497
|
+
async refreshSubscribedCodecs() {
|
|
498
|
+
if (this.subscribedCodecs) {
|
|
499
|
+
return this.setPublishingCodecs(this.subscribedCodecs).catch((e) =>
|
|
500
|
+
this.log.warn(`failed to refresh codec for track`, { ...this.logContext, error: e }),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
491
505
|
/**
|
|
492
506
|
* @internal
|
|
493
507
|
* Sets codecs that should be publishing, returns new codecs that have not yet
|
|
@@ -20,6 +20,13 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
20
20
|
|
|
21
21
|
private webAudioPluginNodes: AudioNode[];
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* the nodes of the graph currently wired up, in order, so teardown can undo exactly the edges
|
|
25
|
+
* `connectWebAudio` created. Reading `webAudioPluginNodes` instead would miss the old plugins
|
|
26
|
+
* once `setWebAudioPlugins` has replaced them.
|
|
27
|
+
*/
|
|
28
|
+
private connectedNodes: AudioNode[] = [];
|
|
29
|
+
|
|
23
30
|
private sinkId?: string;
|
|
24
31
|
|
|
25
32
|
constructor(
|
|
@@ -42,10 +49,11 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
42
49
|
* sets the volume for all attached audio elements
|
|
43
50
|
*/
|
|
44
51
|
setVolume(volume: number) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
if (this.audioContext) {
|
|
53
|
+
// playback goes through the web audio graph, the attached elements themselves stay silent
|
|
54
|
+
this.gainNode?.gain.setTargetAtTime(volume, 0, 0.1);
|
|
55
|
+
} else {
|
|
56
|
+
for (const el of this.attachedElements) {
|
|
49
57
|
el.volume = volume;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -60,7 +68,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
60
68
|
* gets the volume of attached audio elements (loudest)
|
|
61
69
|
*/
|
|
62
70
|
getVolume(): number {
|
|
63
|
-
if (this.elementVolume) {
|
|
71
|
+
if (this.elementVolume !== undefined) {
|
|
64
72
|
return this.elementVolume;
|
|
65
73
|
}
|
|
66
74
|
if (isReactNative()) {
|
|
@@ -96,7 +104,9 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
96
104
|
attach(): HTMLMediaElement;
|
|
97
105
|
attach(element: HTMLMediaElement): HTMLMediaElement;
|
|
98
106
|
attach(element?: HTMLMediaElement): HTMLMediaElement {
|
|
99
|
-
|
|
107
|
+
// the graph is fed by the track rather than by a specific element, so one connection covers
|
|
108
|
+
// all attached elements
|
|
109
|
+
const needsNewWebAudioConnection = !this.sourceNode;
|
|
100
110
|
if (!element) {
|
|
101
111
|
element = super.attach();
|
|
102
112
|
} else {
|
|
@@ -108,14 +118,18 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
108
118
|
this.log.error('Failed to set sink id on remote audio track', e, this.logContext);
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
|
-
if (this.audioContext
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
if (this.audioContext) {
|
|
122
|
+
// audio is routed through the web audio graph, so every attached element has to stay
|
|
123
|
+
// silent, no matter whether it is the one the graph was built from
|
|
114
124
|
element.volume = 0;
|
|
115
125
|
element.muted = true;
|
|
126
|
+
if (needsNewWebAudioConnection) {
|
|
127
|
+
this.log.debug('using audio context mapping', this.logContext);
|
|
128
|
+
this.connectWebAudio(this.audioContext, element);
|
|
129
|
+
}
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
if (this.elementVolume) {
|
|
132
|
+
if (this.elementVolume !== undefined) {
|
|
119
133
|
// make sure volume setting is being applied to the newly attached element
|
|
120
134
|
this.setVolume(this.elementVolume);
|
|
121
135
|
}
|
|
@@ -158,11 +172,26 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
158
172
|
* @experimental
|
|
159
173
|
*/
|
|
160
174
|
setAudioContext(audioContext: AudioContext | undefined) {
|
|
175
|
+
if (this.audioContext === audioContext) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
161
178
|
this.audioContext = audioContext;
|
|
162
|
-
if (audioContext
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
if (audioContext) {
|
|
180
|
+
// playback moves into the web audio graph, mute the elements to avoid double playback
|
|
181
|
+
for (const element of this.attachedElements) {
|
|
182
|
+
element.volume = 0;
|
|
183
|
+
element.muted = true;
|
|
184
|
+
}
|
|
185
|
+
if (this.attachedElements.length > 0) {
|
|
186
|
+
this.connectWebAudio(audioContext, this.attachedElements[0]);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
165
189
|
this.disconnectWebAudio();
|
|
190
|
+
// playback moves back into the elements, which `attach` had muted in favour of the graph
|
|
191
|
+
for (const element of this.attachedElements) {
|
|
192
|
+
element.muted = false;
|
|
193
|
+
element.volume = this.elementVolume ?? 1;
|
|
194
|
+
}
|
|
166
195
|
}
|
|
167
196
|
}
|
|
168
197
|
|
|
@@ -183,15 +212,18 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
183
212
|
// @ts-ignore attached elements always have a srcObject set
|
|
184
213
|
this.sourceNode = context.createMediaStreamSource(element.srcObject);
|
|
185
214
|
let lastNode: AudioNode = this.sourceNode;
|
|
215
|
+
this.connectedNodes = [this.sourceNode];
|
|
186
216
|
this.webAudioPluginNodes.forEach((node) => {
|
|
187
217
|
lastNode.connect(node);
|
|
188
218
|
lastNode = node;
|
|
219
|
+
this.connectedNodes.push(node);
|
|
189
220
|
});
|
|
190
221
|
this.gainNode = context.createGain();
|
|
191
222
|
lastNode.connect(this.gainNode);
|
|
223
|
+
this.connectedNodes.push(this.gainNode);
|
|
192
224
|
this.gainNode.connect(context.destination);
|
|
193
225
|
|
|
194
|
-
if (this.elementVolume) {
|
|
226
|
+
if (this.elementVolume !== undefined) {
|
|
195
227
|
this.gainNode.gain.setTargetAtTime(this.elementVolume, 0, 0.1);
|
|
196
228
|
}
|
|
197
229
|
|
|
@@ -214,6 +246,19 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
214
246
|
}
|
|
215
247
|
|
|
216
248
|
private disconnectWebAudio() {
|
|
249
|
+
// the plugin nodes belong to whoever passed them in and may feed graphs of their own, so only
|
|
250
|
+
// the edges this track created come down. A bare `node.disconnect()` would drop the
|
|
251
|
+
// application's own routing with them
|
|
252
|
+
for (let i = 0; i < this.connectedNodes.length - 1; i += 1) {
|
|
253
|
+
try {
|
|
254
|
+
this.connectedNodes[i].disconnect(this.connectedNodes[i + 1]);
|
|
255
|
+
} catch {
|
|
256
|
+
// a plugin node the application already disconnected itself, the rest of the chain still
|
|
257
|
+
// has to come apart
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.connectedNodes = [];
|
|
261
|
+
// the source and gain nodes are ours alone, so every remaining edge of theirs is ours to drop
|
|
217
262
|
this.gainNode?.disconnect();
|
|
218
263
|
this.sourceNode?.disconnect();
|
|
219
264
|
this.gainNode = undefined;
|
|
@@ -98,6 +98,216 @@ describe('RemoteVideoTrack', () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
describe('Document Picture-in-Picture detection', () => {
|
|
102
|
+
/**
|
|
103
|
+
* happy-dom's MediaStream implements everything except getTracks(), which Track.attach()
|
|
104
|
+
* reads. Subclassing keeps it assignable to the element's srcObject, which type checks the
|
|
105
|
+
* value against happy-dom's own MediaStream.
|
|
106
|
+
*/
|
|
107
|
+
class TestMediaStream extends MediaStream {
|
|
108
|
+
getTracks() {
|
|
109
|
+
return [...this.getAudioTracks(), ...this.getVideoTracks()];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Stands in for window.documentPictureInPicture, which happy-dom does not implement. */
|
|
114
|
+
class FakeDocumentPictureInPicture extends EventTarget {
|
|
115
|
+
window?: Window;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let documentPiP: FakeDocumentPictureInPicture;
|
|
119
|
+
|
|
120
|
+
/** Gives an element the layout box that isElementInViewport reads. */
|
|
121
|
+
function placeElement(
|
|
122
|
+
el: HTMLElement,
|
|
123
|
+
{ top, left, width, height }: { top: number; left: number; width: number; height: number },
|
|
124
|
+
) {
|
|
125
|
+
Object.defineProperties(el, {
|
|
126
|
+
offsetTop: { value: top, configurable: true },
|
|
127
|
+
offsetLeft: { value: left, configurable: true },
|
|
128
|
+
offsetWidth: { value: width, configurable: true },
|
|
129
|
+
offsetHeight: { value: height, configurable: true },
|
|
130
|
+
offsetParent: { value: null, configurable: true },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createVideo(
|
|
135
|
+
ownerDocument: Document,
|
|
136
|
+
box = { top: 40, left: 40, width: 320, height: 180 },
|
|
137
|
+
) {
|
|
138
|
+
const video = ownerDocument.createElement('video');
|
|
139
|
+
ownerDocument.body.appendChild(video);
|
|
140
|
+
placeElement(video, box);
|
|
141
|
+
return video;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Creates a frame in the given document and returns its document. happy-dom implements
|
|
146
|
+
* neither frameElement nor a correct top on a frame's window, so both are wired up here.
|
|
147
|
+
*/
|
|
148
|
+
function createFrame(parentDocument: Document, topWindow: Window) {
|
|
149
|
+
const frame = parentDocument.createElement('iframe');
|
|
150
|
+
parentDocument.body.appendChild(frame);
|
|
151
|
+
const frameDocument = frame.contentDocument!;
|
|
152
|
+
Object.defineProperties(frameDocument.defaultView!, {
|
|
153
|
+
frameElement: { value: frame, configurable: true },
|
|
154
|
+
top: { value: topWindow, configurable: true },
|
|
155
|
+
});
|
|
156
|
+
return frameDocument;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Makes a frame document look like one reached through a cross-origin frame: the containing
|
|
161
|
+
* frame belongs to another origin, so frameElement reads null while top stays readable.
|
|
162
|
+
*/
|
|
163
|
+
function hideContainingFrame(frameDocument: Document) {
|
|
164
|
+
Object.defineProperty(frameDocument.defaultView!, 'frameElement', {
|
|
165
|
+
value: null,
|
|
166
|
+
configurable: true,
|
|
167
|
+
});
|
|
168
|
+
return frameDocument;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Opens a Document PiP window and fires the 'enter' event the SDK listens for. */
|
|
172
|
+
function openPiPWindow({ width, height } = { width: 1280, height: 820 }) {
|
|
173
|
+
const pipDocument = document.implementation.createHTMLDocument('pip');
|
|
174
|
+
documentPiP.window = Object.assign(new EventTarget(), {
|
|
175
|
+
document: pipDocument,
|
|
176
|
+
innerWidth: width,
|
|
177
|
+
innerHeight: height,
|
|
178
|
+
pageXOffset: 0,
|
|
179
|
+
pageYOffset: 0,
|
|
180
|
+
}) as unknown as Window;
|
|
181
|
+
documentPiP.dispatchEvent(new Event('enter'));
|
|
182
|
+
return pipDocument;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Attaches a fresh track to the element and records every VisibilityChanged emission. */
|
|
186
|
+
function attachTrack(video: HTMLElement) {
|
|
187
|
+
const track = new RemoteVideoTrack(new MockMediaStreamTrack(), 'sid', undefined!, {});
|
|
188
|
+
const events: boolean[] = [];
|
|
189
|
+
track.on(TrackEvent.VisibilityChanged, (visible) => events.push(visible));
|
|
190
|
+
track.attach(video as HTMLMediaElement);
|
|
191
|
+
return events;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Flushes the microtask + animation frame that onEnterPiP defers its re-check behind. */
|
|
195
|
+
async function flushDeferredPiPCheck() {
|
|
196
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function setOpenerHidden() {
|
|
200
|
+
Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
vi.stubGlobal('MediaStream', TestMediaStream);
|
|
205
|
+
documentPiP = new FakeDocumentPictureInPicture();
|
|
206
|
+
(window as any).documentPictureInPicture = documentPiP;
|
|
207
|
+
// The opener tab is backgrounded in every case, so pauseVideoInBackground (on by default)
|
|
208
|
+
// reports the element hidden unless it is genuinely in PiP.
|
|
209
|
+
setOpenerHidden();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
vi.unstubAllGlobals();
|
|
214
|
+
delete (window as any).documentPictureInPicture;
|
|
215
|
+
Object.defineProperty(document, 'visibilityState', { value: 'visible', configurable: true });
|
|
216
|
+
document.body.innerHTML = '';
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not report an opener-document element as being in PiP', () => {
|
|
220
|
+
openPiPWindow();
|
|
221
|
+
const video = createVideo(document);
|
|
222
|
+
|
|
223
|
+
// The top-window check must not open a hole here: the opener's top is the opener itself.
|
|
224
|
+
expect(window.top).toBe(window);
|
|
225
|
+
expect(window.top).not.toBe(documentPiP.window);
|
|
226
|
+
// The element sits at (40, 40) of the OPENER page, which falls inside the PiP window's
|
|
227
|
+
// 1280x820 box. It is not in PiP, so the background pause applies.
|
|
228
|
+
expect(attachTrack(video)).toEqual([false]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does not report an element in an opener frame as being in PiP', () => {
|
|
232
|
+
openPiPWindow();
|
|
233
|
+
const openerFrame = createFrame(document, window);
|
|
234
|
+
const video = createVideo(openerFrame);
|
|
235
|
+
|
|
236
|
+
// An opener frame tops out at the opener, so neither the top check nor the walk matches.
|
|
237
|
+
expect(openerFrame.defaultView!.top).toBe(window);
|
|
238
|
+
expect(attachTrack(video)).toEqual([false]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('reports an element moved into the PiP document after the enter event', async () => {
|
|
242
|
+
const video = createVideo(document);
|
|
243
|
+
const events = attachTrack(video);
|
|
244
|
+
expect(events).toEqual([false]);
|
|
245
|
+
|
|
246
|
+
const pipDocument = openPiPWindow();
|
|
247
|
+
// The SDK defers its re-check behind a microtask and an animation frame precisely so the
|
|
248
|
+
// embedder can move its subtree across after the enter event fires.
|
|
249
|
+
pipDocument.body.appendChild(video);
|
|
250
|
+
expect(video.ownerDocument).toBe(pipDocument);
|
|
251
|
+
await flushDeferredPiPCheck();
|
|
252
|
+
|
|
253
|
+
expect(events).toEqual([false, true]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('leaves an unrelated opener element hidden when another element enters PiP', async () => {
|
|
257
|
+
const openerVideo = createVideo(document);
|
|
258
|
+
const movedVideo = createVideo(document);
|
|
259
|
+
const openerEvents = attachTrack(openerVideo);
|
|
260
|
+
const movedEvents = attachTrack(movedVideo);
|
|
261
|
+
|
|
262
|
+
const pipDocument = openPiPWindow();
|
|
263
|
+
pipDocument.body.appendChild(movedVideo);
|
|
264
|
+
await flushDeferredPiPCheck();
|
|
265
|
+
|
|
266
|
+
expect(movedEvents).toEqual([false, true]);
|
|
267
|
+
expect(openerEvents).toEqual([false]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('reports an element inside a frame nested in the PiP document', () => {
|
|
271
|
+
const pipDocument = openPiPWindow();
|
|
272
|
+
const video = createVideo(createFrame(pipDocument, documentPiP.window!), {
|
|
273
|
+
top: 10,
|
|
274
|
+
left: 10,
|
|
275
|
+
width: 320,
|
|
276
|
+
height: 180,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(attachTrack(video)).toEqual([true]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('reports an element behind a cross-origin frame nested in the PiP window', () => {
|
|
283
|
+
const pipDocument = openPiPWindow();
|
|
284
|
+
const pipWindow = documentPiP.window!;
|
|
285
|
+
// PiP document (origin A) -> frame (origin B) -> frame (origin A). The opener can reach
|
|
286
|
+
// the innermost document, but its containing frame belongs to B, so frameElement is null
|
|
287
|
+
// there and the same-origin walk cannot get back to the PiP document.
|
|
288
|
+
const crossOriginFrame = createFrame(pipDocument, pipWindow);
|
|
289
|
+
const innerFrame = hideContainingFrame(createFrame(crossOriginFrame, pipWindow));
|
|
290
|
+
const video = createVideo(innerFrame, { top: 10, left: 10, width: 320, height: 180 });
|
|
291
|
+
|
|
292
|
+
expect(innerFrame.defaultView!.frameElement).toBeNull();
|
|
293
|
+
expect(innerFrame.defaultView!.top).toBe(pipWindow);
|
|
294
|
+
expect(attachTrack(video)).toEqual([true]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('does not report an element scrolled out of the PiP window viewport', () => {
|
|
298
|
+
const pipDocument = openPiPWindow();
|
|
299
|
+
const video = createVideo(pipDocument, { top: 2000, left: 40, width: 320, height: 180 });
|
|
300
|
+
|
|
301
|
+
expect(attachTrack(video)).toEqual([false]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('does not report an element as being in PiP when no PiP window is open', () => {
|
|
305
|
+
const video = createVideo(document);
|
|
306
|
+
|
|
307
|
+
expect(attachTrack(video)).toEqual([false]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
101
311
|
class MockElementInfo implements ElementInfo {
|
|
102
312
|
element: object = {};
|
|
103
313
|
|
|
@@ -435,9 +435,35 @@ class HTMLElementInfo implements ElementInfo {
|
|
|
435
435
|
function isElementInPiP(el: HTMLElement) {
|
|
436
436
|
// Simple video PiP
|
|
437
437
|
if (document.pictureInPictureElement === el) return true;
|
|
438
|
-
// Document PiP
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
// Document PiP: the element has to actually live in the PiP window, otherwise an
|
|
439
|
+
// opener-document element whose coordinates happen to fall inside the PiP window's
|
|
440
|
+
// bounds is reported as being in PiP.
|
|
441
|
+
const pipWindow = window.documentPictureInPicture?.window;
|
|
442
|
+
if (pipWindow) {
|
|
443
|
+
return isDocumentInPiPWindow(el.ownerDocument, pipWindow) && isElementInViewport(el, pipWindow);
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Whether the document is the Document PiP window's own document, or the document of a frame
|
|
449
|
+
// nested inside it. The spec allows frames in a PiP window, so an element in one is in PiP:
|
|
450
|
+
// https://wicg.github.io/document-picture-in-picture/#iframes
|
|
451
|
+
function isDocumentInPiPWindow(doc: Document, pipWindow: Window) {
|
|
452
|
+
// A cross-origin frame anywhere between the element and the PiP window hides frameElement
|
|
453
|
+
// from the walk below, but top stays readable across origins and identifies the top-level
|
|
454
|
+
// window the element belongs to. For an opener element that is the opener, not the PiP window.
|
|
455
|
+
if (doc.defaultView?.top === pipWindow) return true;
|
|
456
|
+
let current: Document | null | undefined = doc;
|
|
457
|
+
while (current) {
|
|
458
|
+
if (current === pipWindow.document) return true;
|
|
459
|
+
try {
|
|
460
|
+
// Reading frameElement across a cross-origin boundary throws, which means we cannot
|
|
461
|
+
// establish the ancestry and must not claim the element is in PiP.
|
|
462
|
+
current = current.defaultView?.frameElement?.ownerDocument;
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
441
467
|
return false;
|
|
442
468
|
}
|
|
443
469
|
|
|
@@ -22,6 +22,17 @@ export interface AudioProcessorOptions extends ProcessorOptions<Track.Kind.Audio
|
|
|
22
22
|
*/
|
|
23
23
|
export interface VideoProcessorOptions extends ProcessorOptions<Track.Kind.Video> {}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* A processor is bound to the track it was set on, not to the room, and it does not survive the
|
|
27
|
+
* track being stopped: unpublishing a track or disconnecting from a room destroys its processor.
|
|
28
|
+
* Both paths stop the track by default, so after reconnecting or republishing, call
|
|
29
|
+
* `setProcessor` again on the new track to put the processor back.
|
|
30
|
+
*
|
|
31
|
+
* The exception is `stopLocalTrackOnUnpublish: false`, which keeps the track alive across an
|
|
32
|
+
* unpublish or disconnect. A video processor then survives with it. An audio processor only
|
|
33
|
+
* survives if the audio context came from the application, because a context the SDK created is
|
|
34
|
+
* closed on disconnect and the processor goes with it.
|
|
35
|
+
*/
|
|
25
36
|
export interface TrackProcessor<
|
|
26
37
|
T extends Track.Kind,
|
|
27
38
|
U extends ProcessorOptions<T> = ProcessorOptions<T>,
|