livekit-client 2.20.0 → 2.20.2
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/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 +41 -11
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +744 -178
- 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 +41 -7
- 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.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +1 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +1 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -1
- package/dist/src/frameMetadata/types.d.ts +1 -0
- package/dist/src/frameMetadata/types.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +42 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +46 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +1 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +1 -0
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/record.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +19 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -0
- package/dist/ts4.2/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/e2ee/utils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/types.d.ts +1 -0
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +42 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +46 -4
- package/dist/ts4.2/room/Room.d.ts +1 -0
- package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/utils.d.ts +1 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +19 -0
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +35 -2
- package/src/e2ee/E2eeManager.ts +3 -1
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/utils.ts +1 -0
- package/src/e2ee/worker/DataCryptor.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +1 -0
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
- package/src/e2ee/worker/e2ee.worker.ts +1 -0
- package/src/e2ee/worker/naluUtils.ts +1 -0
- package/src/e2ee/worker/sifPayload.ts +1 -0
- package/src/frameMetadata/frameMetadata.test.ts +83 -0
- package/src/frameMetadata/frameMetadata.ts +8 -0
- package/src/frameMetadata/types.ts +1 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.test.ts +150 -0
- package/src/room/PCTransport.ts +179 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +275 -2
- package/src/room/RTCEngine.ts +277 -71
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.ts +8 -1
- package/src/room/data-track/depacketizer.ts +1 -0
- package/src/room/data-track/frame.ts +1 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/data-track/outgoing/types.ts +1 -0
- package/src/room/data-track/packet/extensions.ts +1 -0
- package/src/room/data-track/packet/index.ts +1 -0
- package/src/room/data-track/packet/serializable.ts +1 -0
- package/src/room/errors.ts +4 -4
- package/src/room/participant/LocalParticipant.ts +17 -6
- package/src/room/participant/Participant.ts +6 -0
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +11 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/track/record.ts +1 -0
- package/src/room/utils.ts +1 -0
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +48 -1
|
@@ -5,6 +5,7 @@ import type { SignalOptions } from '../api/SignalClient';
|
|
|
5
5
|
import { SignalClient } from '../api/SignalClient';
|
|
6
6
|
import type { BaseE2EEManager } from '../e2ee/E2eeManager';
|
|
7
7
|
import type { InternalRoomOptions } from '../options';
|
|
8
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
8
9
|
import PCTransport from './PCTransport';
|
|
9
10
|
import { PCTransportManager } from './PCTransportManager';
|
|
10
11
|
import { DataTrackInfo } from './data-track/types';
|
|
@@ -86,7 +87,6 @@ export default class RTCEngine extends RTCEngine_base {
|
|
|
86
87
|
private midToTrackId;
|
|
87
88
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
88
89
|
private isWaitingForNetworkReconnect;
|
|
89
|
-
private bufferStatusLowClosingFuture;
|
|
90
90
|
constructor(options: InternalRoomOptions);
|
|
91
91
|
/** @internal */
|
|
92
92
|
get logContext(): {
|
|
@@ -153,11 +153,53 @@ export default class RTCEngine extends RTCEngine_base {
|
|
|
153
153
|
/** @internal */
|
|
154
154
|
publishRpcAck(destinationIdentity: string, requestId: string): Promise<void>;
|
|
155
155
|
sendDataPacket(packet: DataPacket, kind: DataChannelKind): Promise<void>;
|
|
156
|
-
sendLossyBytes(bytes: NonSharedUint8Array, kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
|
|
156
|
+
sendLossyBytes(bytes: NonSharedUint8Array, kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>, bufferStatusFullBehavior?: 'drop' | 'wait'): Promise<void>;
|
|
157
157
|
private resendReliableMessagesForResume;
|
|
158
158
|
private updateAndEmitDCBufferStatus;
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
161
|
+
* this is true and block once it goes false.
|
|
162
|
+
*/
|
|
163
|
+
private isBelowHighWaterMark;
|
|
164
|
+
/**
|
|
165
|
+
* Whether the send buffer has drained to its low-water mark. Drives the public
|
|
166
|
+
* {@link EngineEvent.DCBufferStatusChanged} event.
|
|
167
|
+
*/
|
|
168
|
+
private isBelowLowWaterMark;
|
|
169
|
+
/** Per-kind lock serializing senders that have to wait for the buffer to drain. */
|
|
170
|
+
private waitForBufferHeadroomLocks;
|
|
171
|
+
/**
|
|
172
|
+
* Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
|
|
173
|
+
* stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
|
|
174
|
+
* waiter is parked on the channel object it captured at wait entry; if that object is abandoned
|
|
175
|
+
* its events may never fire again, so the abort is what rejects the waiter and releases the
|
|
176
|
+
* headroom lock instead of stranding every future sender behind it.
|
|
177
|
+
*/
|
|
178
|
+
private waiterAbortControllers;
|
|
179
|
+
private waiterAbortSignal;
|
|
180
|
+
/** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
|
|
181
|
+
private invalidateDataChannelWaiters;
|
|
182
|
+
/** Acquires the per-kind headroom lock, resolving with the unlock function. */
|
|
183
|
+
private getBufferHeadroomLock;
|
|
184
|
+
/**
|
|
185
|
+
* Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
|
|
186
|
+
* common single-send entry point. Callers that need to hold the lock across several sends (the
|
|
187
|
+
* resume replay) acquire it via {@link getBufferHeadroomLock} and call
|
|
188
|
+
* {@link waitForBufferHeadroomWithoutLock} per message instead.
|
|
189
|
+
*/
|
|
190
|
+
waitForBufferHeadroomWithLock(kind: DataChannelKind): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
|
|
193
|
+
* needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
|
|
194
|
+
* already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
|
|
195
|
+
* holds the lock across its whole batch and calls this per message so no other sender can
|
|
196
|
+
* interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
|
|
197
|
+
*
|
|
198
|
+
* Serializing through the per-kind lock means that, once the buffer drains, queued callers
|
|
199
|
+
* refill it one at a time (up to the high-water mark) rather than all at once and overflowing
|
|
200
|
+
* the SCTP send buffer (see livekit/client-sdk-js#1995).
|
|
201
|
+
*/
|
|
202
|
+
private waitForBufferHeadroomWithoutLock;
|
|
161
203
|
/**
|
|
162
204
|
* @internal
|
|
163
205
|
*/
|
|
@@ -2,6 +2,7 @@ import { DataPacket_Kind, DisconnectReason, Encryption_Type, MetricsBatch, Parti
|
|
|
2
2
|
import 'webrtc-adapter';
|
|
3
3
|
import type TypedEmitter from 'typed-emitter';
|
|
4
4
|
import type { InternalRoomOptions, RoomConnectOptions, RoomOptions } from '../options';
|
|
5
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
5
6
|
import TypedPromise from '../utils/TypedPromise';
|
|
6
7
|
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
7
8
|
import type { ByteStreamHandler, TextStreamHandler } from './data-stream/incoming/StreamReader';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
1
2
|
import { DataTrackExtensions } from './packet/extensions';
|
|
2
3
|
/** A pair of payload bytes and packet extensions which can be fed into a {@link DataTrackPacketizer}. */
|
|
3
4
|
export type DataTrackFrame = {
|
|
@@ -91,6 +91,13 @@ export default class IncomingDataTrackManager extends IncomingDataTrackManager_b
|
|
|
91
91
|
* @internal */
|
|
92
92
|
queryPublications(): Promise<DataTrackInfo[]>;
|
|
93
93
|
handleTrackPublished(publisherIdentity: Participant['identity'], info: DataTrackInfo): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Detects and handles SID reassignment, which occurs when the publisher
|
|
96
|
+
* republishes its tracks after a full reconnect.
|
|
97
|
+
*
|
|
98
|
+
* Returns `true` if an SID reassignment occurred, `false` otherwise.
|
|
99
|
+
*/
|
|
100
|
+
private handleSidReassigned;
|
|
94
101
|
handleTrackUnpublished(sid: DataTrackSid): void;
|
|
95
102
|
/** SFU notification that handles have been assigned for requested subscriptions. */
|
|
96
103
|
receivedSfuSubscriberHandles(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { DataTrackDeserializeError, DataTrackDeserializeErrorReason } from './errors';
|
|
3
4
|
import Serializable from './serializable';
|
|
4
5
|
export declare enum DataTrackExtensionTag {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { DataTrackHandle } from '../handle';
|
|
3
4
|
import { DataTrackTimestamp, U16_MAX_SIZE, WrapAroundUnsignedInt } from '../utils';
|
|
4
5
|
import { DataTrackDeserializeError, DataTrackSerializeError, DataTrackSerializeErrorReason } from './errors';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { DataTrackSerializeError } from './errors';
|
|
3
4
|
/** An abstract class implementing common behavior related to data track binary serialization. */
|
|
4
5
|
export default abstract class Serializable {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Codec, ParticipantInfo } from '@livekit/protocol';
|
|
2
2
|
import type { InternalRoomOptions } from '../../options';
|
|
3
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
3
4
|
import TypedPromise from '../../utils/TypedPromise';
|
|
4
5
|
import type RTCEngine from '../RTCEngine';
|
|
5
6
|
import type OutgoingDataStreamManager from '../data-stream/outgoing/OutgoingDataStreamManager';
|
|
@@ -2,6 +2,7 @@ import type { SipDTMF } from '@livekit/protocol';
|
|
|
2
2
|
import { DataPacket_Kind, Encryption_Type, ParticipantInfo, ParticipantInfo_Kind as ParticipantKind, ParticipantPermission, ConnectionQuality as ProtoQuality, SubscriptionError } from '@livekit/protocol';
|
|
3
3
|
import type TypedEmitter from 'typed-emitter';
|
|
4
4
|
import type { StructuredLogger } from '../../logger';
|
|
5
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
5
6
|
import type LocalTrackPublication from '../track/LocalTrackPublication';
|
|
6
7
|
import type LocalVideoTrack from '../track/LocalVideoTrack';
|
|
7
8
|
import type RemoteTrack from '../track/RemoteTrack';
|
|
@@ -26,5 +26,12 @@ export declare class ScalabilityMode {
|
|
|
26
26
|
constructor(scalabilityMode: string);
|
|
27
27
|
toString(): string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the appropriate degradation preference for a video track based on its source.
|
|
31
|
+
*
|
|
32
|
+
* - Camera: 'maintain-framerate' (smoother video for real-time communication)
|
|
33
|
+
* - Screen share: 'maintain-resolution' (clarity is critical for reading text/UI)
|
|
34
|
+
* - Other/unknown: 'balanced'
|
|
35
|
+
*/
|
|
29
36
|
export declare function getDefaultDegradationPreference(track: LocalVideoTrack): RTCDegradationPreference;
|
|
30
37
|
//# sourceMappingURL=publishUtils.d.ts.map
|
|
@@ -24,6 +24,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
24
24
|
mute(): Promise<typeof this>;
|
|
25
25
|
unmute(): Promise<typeof this>;
|
|
26
26
|
restartTrack(options?: AudioCaptureOptions): Promise<void>;
|
|
27
|
+
applyConstraints(constraints: Pick<AudioCaptureOptions, 'autoGainControl' | 'noiseSuppression' | 'echoCancellation' | 'voiceIsolation'>): Promise<void>;
|
|
27
28
|
protected restart(constraints?: MediaTrackConstraints, isUnmuting?: boolean): Promise<typeof this>;
|
|
28
29
|
startMonitor(): void;
|
|
29
30
|
protected monitorSender: () => Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { AudioTrackFeature, StreamState as ProtoStreamState, TrackSource, TrackT
|
|
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
|
3
3
|
import type { SignalClient } from '../../api/SignalClient';
|
|
4
4
|
import type { StructuredLogger } from '../../logger';
|
|
5
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
5
6
|
import type { LoggerOptions } from '../types';
|
|
6
7
|
import type { TrackProcessor } from './processor/types';
|
|
7
8
|
export declare enum VideoQuality {
|
|
@@ -71,7 +71,17 @@ export interface TrackPublishDefaults {
|
|
|
71
71
|
*/
|
|
72
72
|
scalabilityMode?: ScalabilityMode;
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
74
|
+
* Controls how the encoder trades off between resolution and framerate
|
|
75
|
+
* when bandwidth is constrained.
|
|
76
|
+
*
|
|
77
|
+
* - 'maintain-framerate': Prioritizes framerate, reduces resolution if needed
|
|
78
|
+
* - 'maintain-resolution': Prioritizes resolution, drops frames if needed
|
|
79
|
+
* - 'balanced': Balances between both
|
|
80
|
+
*
|
|
81
|
+
* If not set, the SDK uses defaults based on track source:
|
|
82
|
+
* - Camera: 'maintain-framerate' (smoother video for real-time communication)
|
|
83
|
+
* - Screen share: 'maintain-resolution' (clarity is critical for text/UI)
|
|
84
|
+
* - Other/unknown: 'balanced'
|
|
75
85
|
*/
|
|
76
86
|
degradationPreference?: RTCDegradationPreference;
|
|
77
87
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatMessage as ChatMessageModel, ClientInfo, ClientInfo_Capability, DisconnectReason, Transcription as TranscriptionModel } from '@livekit/protocol';
|
|
2
2
|
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
3
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
3
4
|
import TypedPromise from '../utils/TypedPromise';
|
|
4
5
|
import type { BrowserDetails } from '../utils/browserParser';
|
|
5
6
|
import type { ConnectionError } from './errors';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint a LiveKit access token for the mock test-server (HS256, dev secret).
|
|
3
|
+
*
|
|
4
|
+
* The mock's signal behavior mode is selected via a participant attribute
|
|
5
|
+
* under the reserved key `lk.mock` (dot notation, matching LiveKit's internal
|
|
6
|
+
* attribute convention), whose value is the JSON control object
|
|
7
|
+
* `{"signal":"<mode>"}` — mirroring the X-Lk-Mock header protocol. Attributes
|
|
8
|
+
* are a standard AccessToken field (a string map), so a dedicated `lk-mock`
|
|
9
|
+
* key coexists with any real metadata/attributes and needs no bespoke token
|
|
10
|
+
* construction. The room name is just a room; it no longer encodes behavior.
|
|
11
|
+
*/
|
|
12
|
+
export interface TokenOptions {
|
|
13
|
+
/** Mock behavior mode (e.g. 'no_pong'); omitted → the mock defaults to 'happy'. */
|
|
14
|
+
signal?: string;
|
|
15
|
+
/** LeaveRequest action the leave modes should send (0=DISCONNECT,1=RESUME,2=RECONNECT). */
|
|
16
|
+
leaveAction?: number;
|
|
17
|
+
room?: string;
|
|
18
|
+
identity?: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
secret?: string;
|
|
21
|
+
ttlSeconds?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createToken(opts?: TokenOptions): Promise<string>;
|
|
24
|
+
/** A syntactically-valid token signed with the WRONG secret — for 401 tests. */
|
|
25
|
+
export declare function createInvalidToken(): Promise<string>;
|
|
26
|
+
//# sourceMappingURL=signalToken.d.ts.map
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
1
2
|
export interface DataPacketItem {
|
|
2
3
|
data: NonSharedUint8Array;
|
|
3
4
|
sequence: number;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the packet has been handed to the data channel. Unsent packets are queued for the
|
|
7
|
+
* resume replay (e.g. sends that landed in a reconnect window) and must never be trimmed by
|
|
8
|
+
* {@link DataPacketBuffer.alignBufferedAmount}, which reasons about the channel's buffered
|
|
9
|
+
* bytes — those only ever contain sent packets.
|
|
10
|
+
*/
|
|
11
|
+
sent: boolean;
|
|
4
12
|
}
|
|
5
13
|
export declare class DataPacketBuffer {
|
|
6
14
|
private buffer;
|
|
7
15
|
private _totalSize;
|
|
16
|
+
private _sentSize;
|
|
8
17
|
push(item: DataPacketItem): void;
|
|
9
18
|
pop(): DataPacketItem | undefined;
|
|
10
19
|
getAll(): DataPacketItem[];
|
|
20
|
+
/** Every queued packet not yet handed to the channel, in sequence order. */
|
|
21
|
+
getUnsent(): DataPacketItem[];
|
|
22
|
+
/** Marks a single queued packet as handed to the channel. */
|
|
23
|
+
markSent(item: DataPacketItem): void;
|
|
24
|
+
/**
|
|
25
|
+
* Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
|
|
26
|
+
* still buffered was sent on the previous channel (or deferred) and must be re-handed to the
|
|
27
|
+
* current one, so none of it counts as sent until the replay actually transmits it.
|
|
28
|
+
*/
|
|
29
|
+
markAllUnsent(): void;
|
|
11
30
|
popToSequence(sequence: number): void;
|
|
12
31
|
alignBufferedAmount(bufferedAmount: number): void;
|
|
13
32
|
get length(): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livekit-client",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.2",
|
|
4
4
|
"description": "JavaScript/TypeScript client SDK for LiveKit",
|
|
5
5
|
"main": "./dist/livekit-client.umd.js",
|
|
6
6
|
"unpkg": "./dist/livekit-client.umd.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"sdp-transform": "^2.15.0",
|
|
58
58
|
"tslib": "2.8.1",
|
|
59
59
|
"typed-emitter": "^2.1.0",
|
|
60
|
-
"webrtc-adapter": "9.0.
|
|
60
|
+
"webrtc-adapter": "9.0.6"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@types/dom-mediacapture-record": "^1"
|
|
@@ -84,6 +84,8 @@
|
|
|
84
84
|
"@types/ua-parser-js": "0.7.39",
|
|
85
85
|
"@typescript-eslint/eslint-plugin": "8.59.4",
|
|
86
86
|
"@typescript-eslint/parser": "8.59.4",
|
|
87
|
+
"@vitest/browser": "^4.1.10",
|
|
88
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
87
89
|
"downlevel-dts": "^0.11.0",
|
|
88
90
|
"eslint": "10.4.0",
|
|
89
91
|
"eslint-config-airbnb-extended": "^2.3.2",
|
|
@@ -95,6 +97,7 @@
|
|
|
95
97
|
"glob": "^13.0.6",
|
|
96
98
|
"happy-dom": "^20.0.0",
|
|
97
99
|
"jsdom": "^26.1.0",
|
|
100
|
+
"playwright": "^1.61.1",
|
|
98
101
|
"prettier": "^3.4.2",
|
|
99
102
|
"publint": "^0.3.21",
|
|
100
103
|
"rollup": "4.60.4",
|
|
@@ -107,7 +110,7 @@
|
|
|
107
110
|
"typescript": "5.9.3",
|
|
108
111
|
"typescript-eslint": "^8.47.0",
|
|
109
112
|
"vite": "7.3.5",
|
|
110
|
-
"vitest": "^4.
|
|
113
|
+
"vitest": "^4.1.10"
|
|
111
114
|
},
|
|
112
115
|
"scripts": {
|
|
113
116
|
"build": "rollup --config --bundleConfigAsCjs && rollup --config rollup.config.worker.js --bundleConfigAsCjs && rollup --config rollup.config.fm-worker.js --bundleConfigAsCjs && pnpm downlevel-dts",
|
|
@@ -120,10 +123,11 @@
|
|
|
120
123
|
"dev": "pnpm examples:demo",
|
|
121
124
|
"lint": "eslint src",
|
|
122
125
|
"test": "vitest run src",
|
|
126
|
+
"test:e2e": "vitest run --config vitest.e2e.config.mts",
|
|
123
127
|
"deploy": "gh-pages -d examples/demo/dist",
|
|
124
128
|
"format": "prettier --write src examples/**/*.ts",
|
|
125
129
|
"format:check": "prettier --check src examples/**/*.ts",
|
|
126
|
-
"throws:check": "pnpm
|
|
130
|
+
"throws:check": "pnpm throws-check 'src/!(*.test).ts' 'src/**/!(*.test).ts'",
|
|
127
131
|
"type:check": "tsc --noEmit",
|
|
128
132
|
"ci:publish": "pnpm type:check && pnpm build && pnpm compat && changeset publish",
|
|
129
133
|
"downlevel-dts": "downlevel-dts ./dist/src ./dist/ts4.2 --to=4.2",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { type LeaveRequest, LeaveRequest_Action } from '@livekit/protocol';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, inject, it, vi } from 'vitest';
|
|
3
|
+
import { ConnectionErrorReason } from '../room/errors';
|
|
4
|
+
import { createInvalidToken, createToken } from '../test/signalToken';
|
|
5
|
+
import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient';
|
|
6
|
+
|
|
7
|
+
// Provided by src/test/signalServerSetup.ts (spawns the livekit-server test-server).
|
|
8
|
+
const serverUrl = inject('serverUrl');
|
|
9
|
+
const unavailable = inject('e2eUnavailable');
|
|
10
|
+
|
|
11
|
+
const defaultOpts = (): SignalOptions => ({
|
|
12
|
+
autoSubscribe: true,
|
|
13
|
+
maxRetries: 0,
|
|
14
|
+
e2eeEnabled: false,
|
|
15
|
+
websocketTimeout: 5_000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
|
|
20
|
+
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|
21
|
+
return Promise.race([
|
|
22
|
+
p,
|
|
23
|
+
new Promise<T>((_, reject) =>
|
|
24
|
+
setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), ms),
|
|
25
|
+
),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolves with the reason passed to onClose (register before the event can fire). */
|
|
30
|
+
function captureClose(client: SignalClient): Promise<string> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
client.onClose = (reason) => resolve(reason);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolves with the LeaveRequest passed to onLeave. */
|
|
37
|
+
function captureLeave(client: SignalClient): Promise<LeaveRequest> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
client.onLeave = (leave) => resolve(leave);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe.skipIf(!!unavailable)('SignalClient e2e', () => {
|
|
44
|
+
// Behaviors that depend on the signal path (v0 legacy vs v1).
|
|
45
|
+
describe.each([
|
|
46
|
+
{ label: 'v0', useV0: true },
|
|
47
|
+
{ label: 'v1', useV0: false },
|
|
48
|
+
])('$label path', ({ useV0 }) => {
|
|
49
|
+
let client: SignalClient;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
client = new SignalClient(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await client.close().catch(() => {});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Mint a token whose metadata selects the server behavior mode, then join. */
|
|
60
|
+
const join = (mode: string, opts: SignalOptions = defaultOpts(), abortSignal?: AbortSignal) =>
|
|
61
|
+
createToken({ signal: mode }).then((token) =>
|
|
62
|
+
client.join(serverUrl, token, opts, abortSignal, useV0),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it('connects and reports CONNECTED with a join response', async () => {
|
|
66
|
+
const res = await join('happy');
|
|
67
|
+
expect(res).toBeTruthy();
|
|
68
|
+
expect(res.pingInterval).toBeGreaterThan(0);
|
|
69
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('stays connected past the ping timeout while the server pongs', async () => {
|
|
73
|
+
const onClose = vi.fn();
|
|
74
|
+
await join('happy');
|
|
75
|
+
client.onClose = onClose;
|
|
76
|
+
// Server ping timeout is 3s; a healthy pong loop must keep us alive.
|
|
77
|
+
await sleep(4_000);
|
|
78
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
79
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('closes with a ping timeout when the server stops ponging', async () => {
|
|
83
|
+
const closed = captureClose(client);
|
|
84
|
+
await join('no_pong');
|
|
85
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
86
|
+
const reason = await withTimeout(closed, 8_000, 'onClose (ping timeout)');
|
|
87
|
+
expect(reason).toContain('ping timeout');
|
|
88
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('surfaces an unexpected transport close while connected', async () => {
|
|
92
|
+
const closed = captureClose(client);
|
|
93
|
+
await join('close_when_connected');
|
|
94
|
+
const reason = await withTimeout(closed, 5_000, 'onClose (transport closed)');
|
|
95
|
+
expect(reason).toBeTruthy();
|
|
96
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('surfaces an abnormal socket drop (1006) while connected', async () => {
|
|
100
|
+
// Server sends the join, then drops the TCP connection with no close
|
|
101
|
+
// handshake -> the browser reports an abnormal 1006 closure, exercising
|
|
102
|
+
// WebSocketStream's error->close path (distinct from the clean 1011 above).
|
|
103
|
+
const closed = captureClose(client);
|
|
104
|
+
await join('drop_when_connected');
|
|
105
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
106
|
+
const reason = await withTimeout(closed, 5_000, 'onClose (abnormal drop)');
|
|
107
|
+
|
|
108
|
+
expect(reason).toBe('Unexpected WS error');
|
|
109
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects the join when the socket closes before the first message', async () => {
|
|
113
|
+
// Upgrade succeeds, then the server closes the WS before sending a join.
|
|
114
|
+
// The client is still establishing, so it rejects rather than hanging.
|
|
115
|
+
const err = await join('close_before_join').then(
|
|
116
|
+
() => undefined,
|
|
117
|
+
(e) => e as Error,
|
|
118
|
+
);
|
|
119
|
+
expect(err).toBeInstanceOf(Error);
|
|
120
|
+
expect((err as { reason?: ConnectionErrorReason }).reason).toBe(
|
|
121
|
+
ConnectionErrorReason.InternalError,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('delivers a server-initiated leave while connected', async () => {
|
|
126
|
+
const left = captureLeave(client);
|
|
127
|
+
await join('leave_when_connected');
|
|
128
|
+
const leave = await withTimeout(left, 5_000, 'onLeave');
|
|
129
|
+
expect(leave).toBeTruthy();
|
|
130
|
+
expect(leave.action).toBe(LeaveRequest_Action.DISCONNECT);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('forwards a non-DISCONNECT leave action verbatim', async () => {
|
|
134
|
+
// SignalClient does not branch on the leave action (RTCEngine does); it
|
|
135
|
+
// must pass it through unchanged. Guards the FSM swap's leave_received
|
|
136
|
+
// effect, which carries leaveAction. Uses RECONNECT to prove it's not
|
|
137
|
+
// defaulted to DISCONNECT.
|
|
138
|
+
const left = captureLeave(client);
|
|
139
|
+
const token = await createToken({
|
|
140
|
+
signal: 'leave_when_connected',
|
|
141
|
+
leaveAction: LeaveRequest_Action.RECONNECT,
|
|
142
|
+
});
|
|
143
|
+
await client.join(serverUrl, token, defaultOpts(), undefined, useV0);
|
|
144
|
+
const leave = await withTimeout(left, 5_000, 'onLeave');
|
|
145
|
+
expect(leave.action).toBe(LeaveRequest_Action.RECONNECT);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rejects the join when a leave arrives as the first message', async () => {
|
|
149
|
+
const err = await join('leave_first_message').then(
|
|
150
|
+
() => undefined,
|
|
151
|
+
(e) => e as Error,
|
|
152
|
+
);
|
|
153
|
+
expect(err).toBeInstanceOf(Error);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('closes gracefully via close() without firing onClose', async () => {
|
|
157
|
+
const onClose = vi.fn();
|
|
158
|
+
await join('happy');
|
|
159
|
+
client.onClose = onClose;
|
|
160
|
+
await client.close();
|
|
161
|
+
expect(client.isDisconnected).toBe(true);
|
|
162
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('reconnects (resume) back to CONNECTED', async () => {
|
|
166
|
+
await join('happy');
|
|
167
|
+
const token = await createToken({ signal: 'happy' });
|
|
168
|
+
await client.reconnect(serverUrl, token, 'RM_session');
|
|
169
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('rejects a reconnect when a leave arrives as the first message', async () => {
|
|
173
|
+
// Reconnecting into a room that sends leave-first exercises the client's
|
|
174
|
+
// first-message validation while in RECONNECTING (path-independent: it
|
|
175
|
+
// doesn't rely on the mock detecting reconnect, which v1 hides inside the
|
|
176
|
+
// gzipped join_request the mock ignores).
|
|
177
|
+
await join('happy');
|
|
178
|
+
const token = await createToken({ signal: 'leave_first_message' });
|
|
179
|
+
const err = await client.reconnect(serverUrl, token, 'RM_session').then(
|
|
180
|
+
() => undefined,
|
|
181
|
+
(e) => e as Error,
|
|
182
|
+
);
|
|
183
|
+
expect(err).toBeInstanceOf(Error);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- validate-endpoint classification -------------------------------
|
|
187
|
+
it('classifies an invalid token as NotAllowed', async () => {
|
|
188
|
+
const token = await createInvalidToken();
|
|
189
|
+
const err = await client
|
|
190
|
+
.join(serverUrl, token, defaultOpts(), undefined, useV0)
|
|
191
|
+
.catch((e) => e);
|
|
192
|
+
expect(err.reason).toBe(ConnectionErrorReason.NotAllowed);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('classifies room-not-found as NotAllowed', async () => {
|
|
196
|
+
const err = await join('room_not_found').catch((e) => e);
|
|
197
|
+
expect(err.reason).toBe(ConnectionErrorReason.NotAllowed);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('classifies a wrong-path 404 as ServiceNotFound', async () => {
|
|
201
|
+
const err = await join('validate_service_not_found').catch((e) => e);
|
|
202
|
+
expect(err.reason).toBe(ConnectionErrorReason.ServiceNotFound);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('surfaces a 5xx validate as a WebSocket error (ws error shadows 5xx→internal)', async () => {
|
|
206
|
+
// Current behavior: handleConnectionError only maps a 5xx to InternalError
|
|
207
|
+
// when the ws rejection is NOT a ConnectionError. WebSocketStream always
|
|
208
|
+
// rejects with one, so for a refused upgrade the WS error wins. Only
|
|
209
|
+
// 401/403/404 (handled before that check) override it.
|
|
210
|
+
const err = await join('validate_500').catch((e) => e);
|
|
211
|
+
expect(err.reason).toBe(ConnectionErrorReason.WebSocket);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Path-independent transport-level failures (run once, via v0).
|
|
216
|
+
describe('transport failures', () => {
|
|
217
|
+
let client: SignalClient;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
client = new SignalClient(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterEach(async () => {
|
|
224
|
+
await client.close().catch(() => {});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('classifies an unreachable server as ServerUnreachable', async () => {
|
|
228
|
+
const token = await createToken({ signal: 'happy' });
|
|
229
|
+
// Nothing listening on this port -> ws fails, validate fetch fails.
|
|
230
|
+
const err = await client
|
|
231
|
+
.join('ws://127.0.0.1:59999', token, defaultOpts(), undefined, true)
|
|
232
|
+
.catch((e) => e);
|
|
233
|
+
expect(err.reason).toBe(ConnectionErrorReason.ServerUnreachable);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('cancels the connect when the AbortSignal fires mid-connection', async () => {
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const token = await createToken({ signal: 'no_first_message' });
|
|
239
|
+
const p = client.join(serverUrl, token, defaultOpts(), controller.signal, true);
|
|
240
|
+
setTimeout(() => controller.abort('user requested abort'), 300);
|
|
241
|
+
const err = await p.catch((e) => e);
|
|
242
|
+
expect(err.reason).toBe(ConnectionErrorReason.Cancelled);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('times out when the socket opens but no first message arrives', async () => {
|
|
246
|
+
// wsTimeout only guards the WebSocket upgrade and is cleared once ws.opened
|
|
247
|
+
// resolves. If the server accepts the socket then stays silent, the wait for
|
|
248
|
+
// the first message is guarded by its own JOIN_RESPONSE_TIMEOUT (5s), so
|
|
249
|
+
// join() rejects with Timeout instead of hanging.
|
|
250
|
+
const token = await createToken({ signal: 'no_first_message' });
|
|
251
|
+
const joinPromise = client.join(serverUrl, token, defaultOpts(), undefined, true);
|
|
252
|
+
|
|
253
|
+
const err = await withTimeout(
|
|
254
|
+
joinPromise.then(
|
|
255
|
+
() => undefined,
|
|
256
|
+
(e) => e,
|
|
257
|
+
),
|
|
258
|
+
8_000,
|
|
259
|
+
'join to time out',
|
|
260
|
+
);
|
|
261
|
+
expect(err.reason).toBe(ConnectionErrorReason.Timeout);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/api/SignalClient.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { ConnectionError } from '../room/errors';
|
|
|
61
61
|
import CriticalTimers from '../room/timers';
|
|
62
62
|
import type { LoggerOptions } from '../room/types';
|
|
63
63
|
import { getClientInfo, isCompressionStreamSupported, isReactNative, sleep } from '../room/utils';
|
|
64
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
64
65
|
import { AsyncQueue } from '../utils/AsyncQueue';
|
|
65
66
|
import { type WebSocketConnection, WebSocketStream } from './WebSocketStream';
|
|
66
67
|
import {
|
|
@@ -120,6 +121,11 @@ export enum SignalConnectionState {
|
|
|
120
121
|
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
121
122
|
const MAX_WS_CLOSE_TIME = 250;
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* How long (in ms) to wait for the first message after the WebSocket upgrade.
|
|
126
|
+
*/
|
|
127
|
+
const JOIN_RESPONSE_TIMEOUT = 5_000;
|
|
128
|
+
|
|
123
129
|
/** @internal */
|
|
124
130
|
export class SignalClient {
|
|
125
131
|
requestQueue: AsyncQueue;
|
|
@@ -407,7 +413,7 @@ export class SignalClient {
|
|
|
407
413
|
state: this.state,
|
|
408
414
|
});
|
|
409
415
|
if (this.state === SignalConnectionState.CONNECTED) {
|
|
410
|
-
this.handleOnClose(closeInfo.reason
|
|
416
|
+
this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
|
|
411
417
|
}
|
|
412
418
|
}
|
|
413
419
|
return;
|
|
@@ -440,7 +446,34 @@ export class SignalClient {
|
|
|
440
446
|
}
|
|
441
447
|
const signalReader = connection.readable.getReader();
|
|
442
448
|
this.streamWriter = connection.writable.getWriter();
|
|
443
|
-
|
|
449
|
+
|
|
450
|
+
// wsTimeout only guarded the upgrade; guard the first-message read with
|
|
451
|
+
// its own timeout so a silent server can't hang join() forever.
|
|
452
|
+
let firstMessage: ReadableStreamReadResult<string | ArrayBuffer>;
|
|
453
|
+
let firstMessageTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
454
|
+
try {
|
|
455
|
+
firstMessage = await Promise.race([
|
|
456
|
+
signalReader.read(),
|
|
457
|
+
new Promise<never>((_, rejectRead) => {
|
|
458
|
+
firstMessageTimeout = setTimeout(() => {
|
|
459
|
+
rejectRead(
|
|
460
|
+
ConnectionError.timeout(
|
|
461
|
+
'signal connection timed out while waiting for the first message',
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
}, JOIN_RESPONSE_TIMEOUT);
|
|
465
|
+
}),
|
|
466
|
+
]);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
// No first message in time: release the reader and tear down the ws
|
|
469
|
+
// so we surface the timeout instead of leaking an open connection.
|
|
470
|
+
signalReader.releaseLock();
|
|
471
|
+
reject(e);
|
|
472
|
+
this.close();
|
|
473
|
+
return;
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(firstMessageTimeout);
|
|
476
|
+
}
|
|
444
477
|
signalReader.releaseLock();
|
|
445
478
|
if (!firstMessage.value) {
|
|
446
479
|
throw ConnectionError.internal('no message received as first message');
|