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
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import type { InternalRoomOptions } from '../../options';
|
|
33
33
|
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
34
34
|
import TypedPromise from '../../utils/TypedPromise';
|
|
35
|
+
import DeviceManager from '../DeviceManager';
|
|
35
36
|
import { PCTransportState } from '../PCTransportManager';
|
|
36
37
|
import type RTCEngine from '../RTCEngine';
|
|
37
38
|
import { DataChannelKind } from '../RTCEngine';
|
|
@@ -96,11 +97,14 @@ import {
|
|
|
96
97
|
import {
|
|
97
98
|
Future,
|
|
98
99
|
isAudioTrack,
|
|
100
|
+
isChromiumBased,
|
|
101
|
+
isDeviceAcquisitionFailure,
|
|
99
102
|
isE2EESimulcastSupported,
|
|
100
103
|
isFireFox,
|
|
101
104
|
isLocalAudioTrack,
|
|
102
105
|
isLocalTrack,
|
|
103
106
|
isLocalVideoTrack,
|
|
107
|
+
isPermissionDeniedError,
|
|
104
108
|
isSVCCodec,
|
|
105
109
|
isSVCSimulcast,
|
|
106
110
|
isSVCSimulcastSupportedByServer,
|
|
@@ -691,19 +695,21 @@ export default class LocalParticipant extends Participant {
|
|
|
691
695
|
loggerName: this.roomOptions.loggerName,
|
|
692
696
|
loggerContextCb: () => this.logContext,
|
|
693
697
|
});
|
|
694
|
-
const localTracks =
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
698
|
+
const localTracks = await Promise.all(
|
|
699
|
+
tracks.map(async (track) => {
|
|
700
|
+
if (isAudioTrack(track)) {
|
|
701
|
+
this.microphoneError = undefined;
|
|
702
|
+
await track.setAudioContext(this.audioContext);
|
|
703
|
+
track.source = Track.Source.Microphone;
|
|
704
|
+
this.emit(ParticipantEvent.AudioStreamAcquired);
|
|
705
|
+
}
|
|
706
|
+
if (isVideoTrack(track)) {
|
|
707
|
+
this.cameraError = undefined;
|
|
708
|
+
track.source = Track.Source.Camera;
|
|
709
|
+
}
|
|
710
|
+
return track;
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
707
713
|
return localTracks;
|
|
708
714
|
} catch (err) {
|
|
709
715
|
if (err instanceof Error) {
|
|
@@ -819,7 +825,7 @@ export default class LocalParticipant extends Participant {
|
|
|
819
825
|
hasRetriedAfterNegotiationError = false,
|
|
820
826
|
): Promise<LocalTrackPublication> {
|
|
821
827
|
if (isLocalAudioTrack(track)) {
|
|
822
|
-
track.setAudioContext(this.audioContext);
|
|
828
|
+
await track.setAudioContext(this.audioContext);
|
|
823
829
|
}
|
|
824
830
|
|
|
825
831
|
await this.reconnectFuture?.promise;
|
|
@@ -1532,21 +1538,37 @@ export default class LocalParticipant extends Participant {
|
|
|
1532
1538
|
}
|
|
1533
1539
|
const packetTrailerFeatures = this.normalizeRequestedFrameMetadataOptions(track, opts);
|
|
1534
1540
|
|
|
1541
|
+
// the backup sender encodes the processed track, not the raw capture track.dimensions reports
|
|
1542
|
+
const { width, height } = track.mediaStreamTrack.getSettings();
|
|
1543
|
+
const dims = width && height ? { width, height } : undefined;
|
|
1544
|
+
if (!dims) {
|
|
1545
|
+
this.log.warn(
|
|
1546
|
+
'could not determine track dimensions, publishing backup codec without layers',
|
|
1547
|
+
getLogContextFromTrack(track),
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
// the server reads the layers off the codec entry for a request that carries a sid, and
|
|
1551
|
+
// falls back to the top level ones when it predates per codec layers
|
|
1552
|
+
const layers = dims ? videoLayersFromEncodings(dims.width, dims.height, encodings) : [];
|
|
1553
|
+
|
|
1535
1554
|
const req = new AddTrackRequest({
|
|
1536
1555
|
cid: simulcastTrack.mediaStreamTrack.id,
|
|
1537
1556
|
type: Track.kindToProto(track.kind),
|
|
1538
1557
|
muted: track.isMuted,
|
|
1539
1558
|
source: Track.sourceToProto(track.source),
|
|
1540
1559
|
sid: track.sid,
|
|
1560
|
+
width: dims?.width,
|
|
1561
|
+
height: dims?.height,
|
|
1541
1562
|
packetTrailerFeatures,
|
|
1563
|
+
layers,
|
|
1542
1564
|
simulcastCodecs: [
|
|
1543
1565
|
{
|
|
1544
1566
|
codec: opts.videoCodec,
|
|
1545
1567
|
cid: simulcastTrack.mediaStreamTrack.id,
|
|
1568
|
+
layers,
|
|
1546
1569
|
},
|
|
1547
1570
|
],
|
|
1548
1571
|
});
|
|
1549
|
-
req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
|
|
1550
1572
|
|
|
1551
1573
|
if (!this.engine || this.engine.isClosed) {
|
|
1552
1574
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
|
@@ -2140,8 +2162,7 @@ export default class LocalParticipant extends Participant {
|
|
|
2140
2162
|
getLogContextFromTrack(track),
|
|
2141
2163
|
);
|
|
2142
2164
|
if (isLocalAudioTrack(track)) {
|
|
2143
|
-
|
|
2144
|
-
await track.restartTrack({ deviceId: 'default' });
|
|
2165
|
+
await this.restartOnDefaultAudioDevice(track);
|
|
2145
2166
|
} else {
|
|
2146
2167
|
await track.restartTrack();
|
|
2147
2168
|
}
|
|
@@ -2153,6 +2174,34 @@ export default class LocalParticipant extends Participant {
|
|
|
2153
2174
|
}
|
|
2154
2175
|
};
|
|
2155
2176
|
|
|
2177
|
+
/**
|
|
2178
|
+
* @internal
|
|
2179
|
+
*/
|
|
2180
|
+
private async restartOnDefaultAudioDevice(track: LocalAudioTrack) {
|
|
2181
|
+
try {
|
|
2182
|
+
if (isChromiumBased()) {
|
|
2183
|
+
// only chrome has the notion of a literal `default` device
|
|
2184
|
+
await track.setDeviceId({ exact: 'default' });
|
|
2185
|
+
} else {
|
|
2186
|
+
// just try to restart for other browsers and try to change the device id as a fallback in the catch clause
|
|
2187
|
+
await track.restartTrack();
|
|
2188
|
+
}
|
|
2189
|
+
} catch (e) {
|
|
2190
|
+
// restart() clears the manually-stopped flag on entry, so retrying a track the user stopped
|
|
2191
|
+
// while the first attempt was in flight would re-open the capture device behind their back
|
|
2192
|
+
if (!isDeviceAcquisitionFailure(e) || isPermissionDeniedError(e) || track.isStopped) {
|
|
2193
|
+
throw e;
|
|
2194
|
+
}
|
|
2195
|
+
this.log.debug(
|
|
2196
|
+
'could not reacquire the audio device, retrying with an enumerated device',
|
|
2197
|
+
getLogContextFromTrack(track),
|
|
2198
|
+
);
|
|
2199
|
+
// permission is already granted at this point, so skip the probe that would re-acquire media
|
|
2200
|
+
const allDevices = await DeviceManager.getInstance().getDevices('audioinput', false);
|
|
2201
|
+
await track.setDeviceId(allDevices?.[0]?.deviceId ?? 'default');
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2156
2205
|
private getPublicationForTrack(
|
|
2157
2206
|
track: LocalTrack | MediaStreamTrack,
|
|
2158
2207
|
): LocalTrackPublication | undefined {
|
|
@@ -363,10 +363,13 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
|
363
363
|
/**
|
|
364
364
|
* @internal
|
|
365
365
|
*/
|
|
366
|
-
setAudioContext(ctx: AudioContext | undefined) {
|
|
366
|
+
async setAudioContext(ctx: AudioContext | undefined) {
|
|
367
367
|
this.audioContext = ctx;
|
|
368
|
-
|
|
369
|
-
(
|
|
368
|
+
await Promise.all(
|
|
369
|
+
Array.from(this.audioTrackPublications.values())
|
|
370
|
+
.map((publication) => publication.track)
|
|
371
|
+
.filter(isAudioTrack)
|
|
372
|
+
.map((track) => track.setAudioContext(ctx)),
|
|
370
373
|
);
|
|
371
374
|
}
|
|
372
375
|
|
|
@@ -74,25 +74,6 @@ export const computeDefaultScreenShareSimulcastPresets = (fromPreset: VideoPrese
|
|
|
74
74
|
);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
// /**
|
|
78
|
-
// *
|
|
79
|
-
// * @internal
|
|
80
|
-
// * @experimental
|
|
81
|
-
// */
|
|
82
|
-
// const computeDefaultMultiCodecSimulcastEncodings = (width: number, height: number) => {
|
|
83
|
-
// // use vp8 as a default
|
|
84
|
-
// const vp8 = determineAppropriateEncoding(false, width, height);
|
|
85
|
-
// const vp9 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.9 };
|
|
86
|
-
// const h264 = { ...vp8, maxBitrate: vp8.maxBitrate * 1.1 };
|
|
87
|
-
// const av1 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.7 };
|
|
88
|
-
// return {
|
|
89
|
-
// vp8,
|
|
90
|
-
// vp9,
|
|
91
|
-
// h264,
|
|
92
|
-
// av1,
|
|
93
|
-
// };
|
|
94
|
-
// };
|
|
95
|
-
|
|
96
77
|
const videoRids = ['q', 'h', 'f'];
|
|
97
78
|
|
|
98
79
|
/* @internal */
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import DeviceManager from '../DeviceManager';
|
|
3
|
+
import type LocalAudioTrack from '../track/LocalAudioTrack';
|
|
4
|
+
import { Track } from '../track/Track';
|
|
5
|
+
import { isChromiumBased, markDeviceAcquisitionFailure } from '../utils';
|
|
6
|
+
import LocalParticipant from './LocalParticipant';
|
|
7
|
+
|
|
8
|
+
vi.mock('../utils', async () => {
|
|
9
|
+
const actual = await vi.importActual('../utils');
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
isChromiumBased: vi.fn(() => true),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type DefaultDeviceTestParticipant = {
|
|
17
|
+
restartOnDefaultAudioDevice: (track: LocalAudioTrack) => Promise<void>;
|
|
18
|
+
log: { debug: ReturnType<typeof vi.fn> };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function makeDefaultDeviceParticipant() {
|
|
22
|
+
const participant = Object.create(
|
|
23
|
+
LocalParticipant.prototype,
|
|
24
|
+
) as unknown as DefaultDeviceTestParticipant;
|
|
25
|
+
participant.log = { debug: vi.fn() };
|
|
26
|
+
return participant;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function errorNamed(name: string) {
|
|
30
|
+
const e = new Error(name);
|
|
31
|
+
e.name = name;
|
|
32
|
+
markDeviceAcquisitionFailure(e);
|
|
33
|
+
return e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeAudioTrack(
|
|
37
|
+
setDeviceId: ReturnType<typeof vi.fn>,
|
|
38
|
+
restartTrack: ReturnType<typeof vi.fn> = vi.fn(),
|
|
39
|
+
isStopped = false,
|
|
40
|
+
) {
|
|
41
|
+
return {
|
|
42
|
+
sid: 'track-sid',
|
|
43
|
+
source: Track.Source.Microphone,
|
|
44
|
+
setDeviceId,
|
|
45
|
+
restartTrack,
|
|
46
|
+
isStopped,
|
|
47
|
+
} as unknown as LocalAudioTrack;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stubAudioDevices(...deviceIds: string[]) {
|
|
51
|
+
const getDevices = vi.fn().mockResolvedValue(deviceIds.map((deviceId) => ({ deviceId })));
|
|
52
|
+
vi.spyOn(DeviceManager, 'getInstance').mockReturnValue({
|
|
53
|
+
getDevices,
|
|
54
|
+
} as unknown as DeviceManager);
|
|
55
|
+
return getDevices;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('LocalParticipant restartOnDefaultAudioDevice', () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.mocked(isChromiumBased).mockReturnValue(true);
|
|
61
|
+
stubAudioDevices('mic-1', 'mic-2');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to an available device when the exact default is rejected', async () => {
|
|
65
|
+
const getDevices = stubAudioDevices('mic-1', 'mic-2');
|
|
66
|
+
const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('OverconstrainedError'));
|
|
67
|
+
const participant = makeDefaultDeviceParticipant();
|
|
68
|
+
|
|
69
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
|
|
70
|
+
|
|
71
|
+
expect(getDevices).toHaveBeenCalledWith('audioinput', false);
|
|
72
|
+
expect(setDeviceId).toHaveBeenCalledTimes(2);
|
|
73
|
+
expect(setDeviceId).toHaveBeenNthCalledWith(1, { exact: 'default' });
|
|
74
|
+
expect(setDeviceId).toHaveBeenNthCalledWith(2, 'mic-1');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('falls back to the default id when no device is enumerated', async () => {
|
|
78
|
+
stubAudioDevices();
|
|
79
|
+
const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('OverconstrainedError'));
|
|
80
|
+
const participant = makeDefaultDeviceParticipant();
|
|
81
|
+
|
|
82
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
|
|
83
|
+
|
|
84
|
+
expect(setDeviceId).toHaveBeenNthCalledWith(2, 'default');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('falls back for a rejection name the spec does not prescribe', async () => {
|
|
88
|
+
const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('ConstraintNotSatisfiedError'));
|
|
89
|
+
const participant = makeDefaultDeviceParticipant();
|
|
90
|
+
|
|
91
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
|
|
92
|
+
|
|
93
|
+
expect(setDeviceId).toHaveBeenCalledTimes(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not retry a denied permission, so one prompt never becomes two', async () => {
|
|
97
|
+
const setDeviceId = vi.fn().mockRejectedValue(errorNamed('NotAllowedError'));
|
|
98
|
+
const participant = makeDefaultDeviceParticipant();
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
|
|
102
|
+
).rejects.toThrow();
|
|
103
|
+
|
|
104
|
+
expect(setDeviceId).toHaveBeenCalledOnce();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('asks for the exact default only, when that succeeds', async () => {
|
|
108
|
+
const setDeviceId = vi.fn().mockResolvedValue(undefined);
|
|
109
|
+
const participant = makeDefaultDeviceParticipant();
|
|
110
|
+
|
|
111
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
|
|
112
|
+
|
|
113
|
+
expect(setDeviceId).toHaveBeenCalledOnce();
|
|
114
|
+
expect(setDeviceId).toHaveBeenCalledWith({ exact: 'default' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('propagates the second rejection, not the first', async () => {
|
|
118
|
+
const setDeviceId = vi
|
|
119
|
+
.fn()
|
|
120
|
+
.mockRejectedValueOnce(errorNamed('OverconstrainedError'))
|
|
121
|
+
.mockRejectedValueOnce(errorNamed('NotReadableError'));
|
|
122
|
+
const participant = makeDefaultDeviceParticipant();
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
|
|
126
|
+
).rejects.toThrow('NotReadableError');
|
|
127
|
+
|
|
128
|
+
expect(setDeviceId).toHaveBeenCalledTimes(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not retry a failure that happened after the device was acquired', async () => {
|
|
132
|
+
const postAcquisition = new Error('processor restart failed');
|
|
133
|
+
const setDeviceId = vi.fn().mockRejectedValue(postAcquisition);
|
|
134
|
+
const participant = makeDefaultDeviceParticipant();
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
|
|
138
|
+
).rejects.toThrow('processor restart failed');
|
|
139
|
+
|
|
140
|
+
expect(setDeviceId).toHaveBeenCalledOnce();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('does not retry a track the user stopped while the first attempt was in flight', async () => {
|
|
144
|
+
const setDeviceId = vi.fn().mockRejectedValue(errorNamed('OverconstrainedError'));
|
|
145
|
+
const participant = makeDefaultDeviceParticipant();
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, vi.fn(), true)),
|
|
149
|
+
).rejects.toThrow();
|
|
150
|
+
|
|
151
|
+
expect(setDeviceId).toHaveBeenCalledOnce();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('keeps the existing device id on a browser without a literal default', async () => {
|
|
155
|
+
vi.mocked(isChromiumBased).mockReturnValue(false);
|
|
156
|
+
const setDeviceId = vi.fn();
|
|
157
|
+
const restartTrack = vi.fn().mockResolvedValue(undefined);
|
|
158
|
+
const participant = makeDefaultDeviceParticipant();
|
|
159
|
+
|
|
160
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, restartTrack));
|
|
161
|
+
|
|
162
|
+
expect(restartTrack).toHaveBeenCalledOnce();
|
|
163
|
+
expect(setDeviceId).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('switches device when a browser without a literal default cannot reacquire', async () => {
|
|
167
|
+
vi.mocked(isChromiumBased).mockReturnValue(false);
|
|
168
|
+
const setDeviceId = vi.fn();
|
|
169
|
+
const restartTrack = vi.fn().mockRejectedValue(errorNamed('OverconstrainedError'));
|
|
170
|
+
const participant = makeDefaultDeviceParticipant();
|
|
171
|
+
|
|
172
|
+
await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, restartTrack));
|
|
173
|
+
|
|
174
|
+
expect(restartTrack).toHaveBeenCalledOnce();
|
|
175
|
+
expect(setDeviceId).toHaveBeenCalledExactlyOnceWith('mic-1');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -255,6 +255,57 @@ describe('RpcClientManager', () => {
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it('cleans up a failed v2 publish without a later ack timeout', async () => {
|
|
259
|
+
vi.useFakeTimers();
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
sendTextMock.mockRejectedValue(new Error('engine closed'));
|
|
263
|
+
await expect(
|
|
264
|
+
rpcClientManager.performRpc({
|
|
265
|
+
destinationIdentity: 'remote-identity',
|
|
266
|
+
method: 'test-method',
|
|
267
|
+
payload: 'test-payload',
|
|
268
|
+
}),
|
|
269
|
+
).rejects.toThrow('engine closed');
|
|
270
|
+
|
|
271
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
272
|
+
expect((rpcClientManager as any).pendingAcks.size).toBe(0);
|
|
273
|
+
expect((rpcClientManager as any).pendingResponses.size).toBe(0);
|
|
274
|
+
await vi.advanceTimersByTimeAsync(7001);
|
|
275
|
+
// An unhandled rejection from completionFuture would also fail Vitest.
|
|
276
|
+
} finally {
|
|
277
|
+
vi.useRealTimers();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('handles an ack timeout while a v2 publish is still pending', async () => {
|
|
282
|
+
vi.useFakeTimers();
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
let resolveSend!: () => void;
|
|
286
|
+
sendTextMock.mockReturnValue(
|
|
287
|
+
new Promise<void>((resolve) => {
|
|
288
|
+
resolveSend = resolve;
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
const performRpcPromise = rpcClientManager.performRpc({
|
|
292
|
+
destinationIdentity: 'remote-identity',
|
|
293
|
+
method: 'test-method',
|
|
294
|
+
payload: 'test-payload',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await vi.advanceTimersByTimeAsync(7001);
|
|
298
|
+
resolveSend();
|
|
299
|
+
const [, completionPromise] = await performRpcPromise;
|
|
300
|
+
await expect(completionPromise).rejects.toThrow(/Connection timeout/i);
|
|
301
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
302
|
+
expect((rpcClientManager as any).pendingAcks.size).toBe(0);
|
|
303
|
+
expect((rpcClientManager as any).pendingResponses.size).toBe(0);
|
|
304
|
+
} finally {
|
|
305
|
+
vi.useRealTimers();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
258
309
|
it('should handle a v2 RPC error response', async () => {
|
|
259
310
|
const errorCode = 101;
|
|
260
311
|
const errorMessage = 'Test error message';
|
|
@@ -108,22 +108,13 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
|
|
|
108
108
|
participantIdentity: destinationIdentity,
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
method,
|
|
115
|
-
payload,
|
|
116
|
-
effectiveTimeoutMs,
|
|
117
|
-
remoteClientProtocol,
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
responseTimeoutId = setTimeout(() => {
|
|
121
|
-
this.pendingResponses.delete(id);
|
|
122
|
-
completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
|
|
123
|
-
}, responseTimeoutMs);
|
|
124
|
-
|
|
111
|
+
// Observe the future before publishing: a slow send can outlast the ack timer.
|
|
112
|
+
// Keep the returned promise rejected for callers, while marking its rejection
|
|
113
|
+
// handled until performRpc has finished publishing and can return it.
|
|
125
114
|
const completionPromise = completionFuture.promise.finally(() => {
|
|
126
|
-
|
|
115
|
+
if (responseTimeoutId !== null) {
|
|
116
|
+
clearTimeout(responseTimeoutId);
|
|
117
|
+
}
|
|
127
118
|
|
|
128
119
|
if (this.pendingAcks.has(id)) {
|
|
129
120
|
this.log.warn('RPC response received before ack', id);
|
|
@@ -131,6 +122,32 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
|
|
|
131
122
|
clearTimeout(ackTimeoutId);
|
|
132
123
|
}
|
|
133
124
|
});
|
|
125
|
+
completionPromise.catch(() => {});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await this.publishRpcRequest(
|
|
129
|
+
destinationIdentity,
|
|
130
|
+
id,
|
|
131
|
+
method,
|
|
132
|
+
payload,
|
|
133
|
+
effectiveTimeoutMs,
|
|
134
|
+
remoteClientProtocol,
|
|
135
|
+
);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// The caller receives the publish error. Do not leave an ack timer that
|
|
138
|
+
// later rejects a future the caller never received.
|
|
139
|
+
clearTimeout(ackTimeoutId);
|
|
140
|
+
this.pendingAcks.delete(id);
|
|
141
|
+
this.pendingResponses.delete(id);
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.pendingResponses.has(id)) {
|
|
146
|
+
responseTimeoutId = setTimeout(() => {
|
|
147
|
+
this.pendingResponses.delete(id);
|
|
148
|
+
completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
|
|
149
|
+
}, responseTimeoutMs);
|
|
150
|
+
}
|
|
134
151
|
|
|
135
152
|
return [id, completionPromise];
|
|
136
153
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RpcError, byteLength, truncateBytes } from './utils';
|
|
3
|
+
|
|
4
|
+
describe('truncateBytes', () => {
|
|
5
|
+
it('returns the string unchanged when it fits', () => {
|
|
6
|
+
expect(truncateBytes('hello', 5)).toBe('hello');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('truncates ASCII on the byte limit', () => {
|
|
10
|
+
expect(truncateBytes('hello world', 5)).toBe('hello');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not split a multibyte character', () => {
|
|
14
|
+
expect(truncateBytes('aéé', 4)).toBe('aé');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('does not leave half of a surrogate pair when truncating emoji', () => {
|
|
18
|
+
// 'a' (1 byte) + 63 emoji (4 bytes each) = 253 bytes, so the limit falls inside the next emoji.
|
|
19
|
+
const str = `a${'😀'.repeat(64)}`;
|
|
20
|
+
const truncated = truncateBytes(str, 256);
|
|
21
|
+
|
|
22
|
+
expect(truncated).toBe(`a${'😀'.repeat(63)}`);
|
|
23
|
+
expect(new TextDecoder().decode(new TextEncoder().encode(truncated))).toBe(truncated);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps an RpcError message valid when it is cut inside an emoji', () => {
|
|
27
|
+
const error = new RpcError(1500, `a${'😀'.repeat(100)}`);
|
|
28
|
+
|
|
29
|
+
expect(byteLength(error.message)).toBeLessThanOrEqual(RpcError.MAX_MESSAGE_BYTES);
|
|
30
|
+
expect(error.message).not.toMatch(/[\ud800-\udbff](?![\udc00-\udfff])/);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/room/rpc/utils.ts
CHANGED
|
@@ -214,5 +214,12 @@ export function truncateBytes(str: string, maxBytes: number): string {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// Never end on the first half of a surrogate pair: it would be encoded as a replacement
|
|
218
|
+
// character, and the truncated string would no longer round trip.
|
|
219
|
+
const lastCode = low > 0 ? str.charCodeAt(low - 1) : 0;
|
|
220
|
+
if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
|
|
221
|
+
low -= 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
217
224
|
return str.slice(0, low);
|
|
218
225
|
}
|
|
@@ -18,6 +18,12 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
18
18
|
|
|
19
19
|
private isKrispNoiseFilterEnabled = false;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* the processed track we currently have krisp feature listeners registered on. Tracked
|
|
23
|
+
* separately, as `processor.processedTrack` can be swapped out while we hold the listeners.
|
|
24
|
+
*/
|
|
25
|
+
private observedProcessedTrack?: MediaStreamTrack;
|
|
26
|
+
|
|
21
27
|
protected processor?: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions> | undefined;
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -184,6 +190,51 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
184
190
|
);
|
|
185
191
|
};
|
|
186
192
|
|
|
193
|
+
/**
|
|
194
|
+
* registers the krisp feature listeners on the given processed track, moving them over from
|
|
195
|
+
* a previously observed one. Passing `undefined` just removes the existing listeners.
|
|
196
|
+
*/
|
|
197
|
+
private observeProcessedTrack(processedTrack: MediaStreamTrack | undefined) {
|
|
198
|
+
if (this.observedProcessedTrack === processedTrack) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (this.observedProcessedTrack) {
|
|
202
|
+
this.observedProcessedTrack.removeEventListener(
|
|
203
|
+
'enable-lk-krisp-noise-filter',
|
|
204
|
+
this.handleKrispNoiseFilterEnable,
|
|
205
|
+
);
|
|
206
|
+
this.observedProcessedTrack.removeEventListener(
|
|
207
|
+
'disable-lk-krisp-noise-filter',
|
|
208
|
+
this.handleKrispNoiseFilterDisable,
|
|
209
|
+
);
|
|
210
|
+
this.observedProcessedTrack = undefined;
|
|
211
|
+
if (this.isKrispNoiseFilterEnabled) {
|
|
212
|
+
// we're no longer observing the track that had the filter enabled, so the feature
|
|
213
|
+
// can't be active anymore. Make sure the state doesn't get stuck on `true`.
|
|
214
|
+
this.handleKrispNoiseFilterDisable();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!processedTrack) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
processedTrack.addEventListener(
|
|
221
|
+
'enable-lk-krisp-noise-filter',
|
|
222
|
+
this.handleKrispNoiseFilterEnable,
|
|
223
|
+
);
|
|
224
|
+
processedTrack.addEventListener(
|
|
225
|
+
'disable-lk-krisp-noise-filter',
|
|
226
|
+
this.handleKrispNoiseFilterDisable,
|
|
227
|
+
);
|
|
228
|
+
this.observedProcessedTrack = processedTrack;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sets a processor on this track.
|
|
233
|
+
*
|
|
234
|
+
* The processor is destroyed when the track is stopped, and also when the audio context goes
|
|
235
|
+
* away, so a disconnect ends it on both counts and it has to be set again after reconnecting.
|
|
236
|
+
* See {@link TrackProcessor} for the full lifecycle.
|
|
237
|
+
*/
|
|
187
238
|
async setProcessor(processor: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>) {
|
|
188
239
|
const unlock = await this.trackChangeLock.lock();
|
|
189
240
|
try {
|
|
@@ -209,14 +260,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
209
260
|
this.processor = processor;
|
|
210
261
|
if (this.processor.processedTrack) {
|
|
211
262
|
await this.sender?.replaceTrack(this.processor.processedTrack);
|
|
212
|
-
this.processor.processedTrack
|
|
213
|
-
'enable-lk-krisp-noise-filter',
|
|
214
|
-
this.handleKrispNoiseFilterEnable,
|
|
215
|
-
);
|
|
216
|
-
this.processor.processedTrack.addEventListener(
|
|
217
|
-
'disable-lk-krisp-noise-filter',
|
|
218
|
-
this.handleKrispNoiseFilterDisable,
|
|
219
|
-
);
|
|
263
|
+
this.observeProcessedTrack(this.processor.processedTrack);
|
|
220
264
|
}
|
|
221
265
|
this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
|
|
222
266
|
} finally {
|
|
@@ -224,12 +268,59 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
224
268
|
}
|
|
225
269
|
}
|
|
226
270
|
|
|
271
|
+
protected override async onMediaStreamTrackChanged(): Promise<void> {
|
|
272
|
+
// the processor rebuilds its graph around the new track, so the krisp listeners have to
|
|
273
|
+
// follow it onto whatever it hands back
|
|
274
|
+
this.observeProcessedTrack(this.processor?.processedTrack);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
protected async internalStopProcessor(keepElement = true) {
|
|
278
|
+
this.observeProcessedTrack(undefined);
|
|
279
|
+
await super.internalStopProcessor(keepElement);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stop() {
|
|
283
|
+
this.observeProcessedTrack(undefined);
|
|
284
|
+
super.stop();
|
|
285
|
+
}
|
|
286
|
+
|
|
227
287
|
/**
|
|
228
288
|
* @internal
|
|
229
289
|
* @experimental
|
|
290
|
+
*
|
|
291
|
+
* A processor's nodes (`AudioWorkletNode`, ...) can't move between contexts, so the processor
|
|
292
|
+
* is rebuilt whenever the context changes and torn down when there is none left to run in.
|
|
230
293
|
*/
|
|
231
|
-
setAudioContext(audioContext: AudioContext | undefined) {
|
|
232
|
-
this.audioContext
|
|
294
|
+
async setAudioContext(audioContext: AudioContext | undefined) {
|
|
295
|
+
if (this.audioContext === audioContext) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const unlock = await this.trackChangeLock.lock();
|
|
299
|
+
try {
|
|
300
|
+
this.audioContext = audioContext;
|
|
301
|
+
if (!this.processor || isReactNative()) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!audioContext) {
|
|
305
|
+
this.log.debug('stopping audio processor, audio context has been removed', this.logContext);
|
|
306
|
+
await this.internalStopProcessor();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.log.debug('restarting audio processor on new audio context', this.logContext);
|
|
310
|
+
await this.processor.restart({
|
|
311
|
+
kind: this.kind,
|
|
312
|
+
track: this._mediaStreamTrack,
|
|
313
|
+
audioContext,
|
|
314
|
+
localTrack: this,
|
|
315
|
+
});
|
|
316
|
+
if (this.processor.processedTrack) {
|
|
317
|
+
await this.sender?.replaceTrack(this.processor.processedTrack);
|
|
318
|
+
this.observeProcessedTrack(this.processor.processedTrack);
|
|
319
|
+
}
|
|
320
|
+
this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
|
|
321
|
+
} finally {
|
|
322
|
+
unlock();
|
|
323
|
+
}
|
|
233
324
|
}
|
|
234
325
|
|
|
235
326
|
async getSenderStats(): Promise<AudioSenderStats | undefined> {
|