livekit-client 2.22.4 → 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/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 +69 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1133 -289
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -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.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/room/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +26 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +53 -0
- 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/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/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 +7 -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.map +1 -1
- package/dist/src/room/utils.d.ts +28 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/WebSocketStream.d.ts +1 -0
- package/dist/ts4.2/api/utils.d.ts +1 -1
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/ts4.2/index.d.ts +2 -2
- package/dist/ts4.2/room/PCTransport.d.ts +26 -4
- package/dist/ts4.2/room/RTCEngine.d.ts +8 -0
- package/dist/ts4.2/room/Room.d.ts +53 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +4 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +18 -0
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/ts4.2/room/track/processor/types.d.ts +11 -0
- package/dist/ts4.2/room/utils.d.ts +28 -1
- package/package.json +1 -1
- package/src/api/SignalClient.test.ts +234 -0
- package/src/api/SignalClient.ts +58 -23
- 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/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +144 -2
- package/src/room/PCTransport.ts +151 -9
- package/src/room/RTCEngine.test.ts +234 -2
- package/src/room/RTCEngine.ts +102 -7
- package/src/room/Room.test.ts +116 -0
- package/src/room/Room.ts +286 -74
- 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 +31 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +11 -0
- 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/restartOnDefaultAudioDevice.test.ts +177 -0
- 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 +70 -13
- package/src/room/track/LocalVideoTrack.ts +7 -0
- package/src/room/track/RemoteAudioTrack.ts +56 -11
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +5 -0
- package/src/room/track/utils.ts +29 -5
- package/src/room/utils.test.ts +126 -0
- package/src/room/utils.ts +84 -21
|
@@ -23,8 +23,9 @@ export interface DataChannelManagerOptions {
|
|
|
23
23
|
* live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
|
|
24
24
|
* connections come and go) plus the subscriber-side receive handles adopted by label.
|
|
25
25
|
*
|
|
26
|
-
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which
|
|
27
|
-
* parked headroom waiters as a built-in — there is no separate
|
|
26
|
+
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which close the
|
|
27
|
+
* released handle and reject parked headroom waiters as a built-in — there is no separate
|
|
28
|
+
* invalidation step to forget.
|
|
28
29
|
*/
|
|
29
30
|
export declare class DataChannelManager {
|
|
30
31
|
readonly reliable: ReliableDataChannel;
|
|
@@ -42,8 +43,8 @@ export declare class DataChannelManager {
|
|
|
42
43
|
get hasPublisherChannels(): boolean;
|
|
43
44
|
/**
|
|
44
45
|
* Creates the three publisher data channels on the given transport, wires their handlers, and
|
|
45
|
-
* attaches them to the wrappers — attaching
|
|
46
|
-
*
|
|
46
|
+
* attaches them to the wrappers — attaching closes any replaced channel objects and rejects the
|
|
47
|
+
* waiters still parked on them.
|
|
47
48
|
*/
|
|
48
49
|
createPublisherChannels(pcManager: PCTransportManager): void;
|
|
49
50
|
/**
|
|
@@ -52,10 +53,10 @@ export declare class DataChannelManager {
|
|
|
52
53
|
*/
|
|
53
54
|
adoptSubscriberChannel(channel: RTCDataChannel): boolean;
|
|
54
55
|
/**
|
|
55
|
-
* Tears down all channels for a peer-connection cleanup:
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Tears down all channels for a peer-connection cleanup: strips handlers, closes the handles,
|
|
57
|
+
* and resets the reliable session state. Detaching the publisher handles closes them and
|
|
58
|
+
* rejects parked waiters (the spec allows `pc.close()` to transition channels to 'closed'
|
|
59
|
+
* without firing events, so waiting for browser close events is not an option).
|
|
59
60
|
*/
|
|
60
61
|
teardown(): void;
|
|
61
62
|
}
|
|
@@ -25,7 +25,8 @@ export interface FlowControlledDataChannelOptions {
|
|
|
25
25
|
* Waiters are parked on the channel object captured at wait entry. If that object stops being
|
|
26
26
|
* current — replaced or torn down — its events may never fire again, so the owner must call
|
|
27
27
|
* {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
|
|
28
|
-
* gets a fresh controller.
|
|
28
|
+
* gets a fresh controller. {@link attach} and {@link detach} do this, and close the released
|
|
29
|
+
* handle, as part of handle turnover.
|
|
29
30
|
*/
|
|
30
31
|
export declare class FlowControlledDataChannel {
|
|
31
32
|
readonly kind: DataChannelKind;
|
|
@@ -42,15 +43,23 @@ export declare class FlowControlledDataChannel {
|
|
|
42
43
|
constructor(opts: FlowControlledDataChannelOptions);
|
|
43
44
|
get channelHandle(): (RTCDataChannel | undefined);
|
|
44
45
|
/**
|
|
45
|
-
* Attaches the channel handle this wrapper controls. Replacing an existing handle
|
|
46
|
-
* parked waiters — their events would never fire again on the abandoned object — and
|
|
47
|
-
* fresh controller
|
|
48
|
-
*
|
|
49
|
-
* waiter structurally impossible.
|
|
46
|
+
* Attaches the channel handle this wrapper controls. Replacing an existing handle closes it and
|
|
47
|
+
* rejects parked waiters — their events would never fire again on the abandoned object — and
|
|
48
|
+
* installs a fresh controller. Wrappers outlive their handles: this is the one place handle
|
|
49
|
+
* turnover happens, so a handle the wrapper lets go of is never left open, and stranding a
|
|
50
|
+
* waiter is structurally impossible.
|
|
50
51
|
*/
|
|
51
52
|
attach(dc: RTCDataChannel): void;
|
|
52
|
-
/** Detaches the handle on teardown, rejecting parked waiters. */
|
|
53
|
+
/** Detaches and closes the handle on teardown, rejecting parked waiters. */
|
|
53
54
|
detach(reason?: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Closes a handle the wrapper no longer controls. `close()` moves it to `closing` synchronously,
|
|
57
|
+
* so senders still holding it (queued on the headroom lock) fail the open check once they get
|
|
58
|
+
* the lock. Parked waiters are aborted directly instead of relying on the `close` event, which
|
|
59
|
+
* fires asynchronously, if at all (the spec lets `pc.close()` skip it). Strip any handlers that
|
|
60
|
+
* must not observe the close before calling this.
|
|
61
|
+
*/
|
|
62
|
+
private release;
|
|
54
63
|
protected getChannel(): RTCDataChannel | undefined;
|
|
55
64
|
/**
|
|
56
65
|
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
@@ -70,16 +79,20 @@ export declare class FlowControlledDataChannel {
|
|
|
70
79
|
*/
|
|
71
80
|
lockHeadroom(): Promise<() => void>;
|
|
72
81
|
/**
|
|
73
|
-
* Resolves once the caller may send on
|
|
74
|
-
*
|
|
82
|
+
* Resolves once the caller may send on `dc`: immediately while its send buffer is at or below
|
|
83
|
+
* the high-water mark, otherwise once the buffer has drained to the low-water mark (the
|
|
75
84
|
* `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
|
|
76
85
|
* the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
|
|
77
86
|
* sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
|
|
78
|
-
*
|
|
87
|
+
* checks run inside the lock so queued callers proceed in FIFO order.
|
|
88
|
+
*
|
|
89
|
+
* The caller passes the handle it resolved before queueing, and the wait is against that one
|
|
90
|
+
* handle throughout: if it was replaced meanwhile, it has been closed, so the wait rejects
|
|
91
|
+
* rather than letting the caller send on the replacement out of turn.
|
|
79
92
|
*/
|
|
80
|
-
waitForHeadroomWithLock(): Promise<void>;
|
|
93
|
+
waitForHeadroomWithLock(dc: RTCDataChannel): Promise<void>;
|
|
81
94
|
/** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
|
|
82
|
-
waitForHeadroomWithoutLock(): Promise<void>;
|
|
95
|
+
waitForHeadroomWithoutLock(dc: RTCDataChannel): Promise<void>;
|
|
83
96
|
/** Rejects all parked headroom waiters; the next waiter gets a fresh controller. */
|
|
84
97
|
invalidateWaiters(reason: string): void;
|
|
85
98
|
/**
|
|
@@ -275,6 +275,10 @@ export default class LocalParticipant extends Participant {
|
|
|
275
275
|
private handleSubscribedQualityUpdate;
|
|
276
276
|
private handleLocalTrackUnpublished;
|
|
277
277
|
private handleTrackEnded;
|
|
278
|
+
/**
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
private restartOnDefaultAudioDevice;
|
|
278
282
|
private getPublicationForTrack;
|
|
279
283
|
private waitForPendingPublicationOfSource;
|
|
280
284
|
/** Publishes a data track.
|
|
@@ -106,7 +106,7 @@ export default class Participant extends Participant_base {
|
|
|
106
106
|
/**
|
|
107
107
|
* @internal
|
|
108
108
|
*/
|
|
109
|
-
setAudioContext(ctx: AudioContext | undefined): void
|
|
109
|
+
setAudioContext(ctx: AudioContext | undefined): Promise<void>;
|
|
110
110
|
protected addTrackPublication(publication: TrackPublication): void;
|
|
111
111
|
}
|
|
112
112
|
export type ParticipantEventCallbacks = {
|
|
@@ -9,6 +9,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
9
9
|
stopOnMute: boolean;
|
|
10
10
|
private prevStats?;
|
|
11
11
|
private isKrispNoiseFilterEnabled;
|
|
12
|
+
/**
|
|
13
|
+
* the processed track we currently have krisp feature listeners registered on. Tracked
|
|
14
|
+
* separately, as `processor.processedTrack` can be swapped out while we hold the listeners.
|
|
15
|
+
*/
|
|
16
|
+
private observedProcessedTrack?;
|
|
12
17
|
protected processor?: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions> | undefined;
|
|
13
18
|
get enhancedNoiseCancellation(): boolean;
|
|
14
19
|
/**
|
|
@@ -27,12 +32,30 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
27
32
|
protected monitorSender: () => Promise<void>;
|
|
28
33
|
private handleKrispNoiseFilterEnable;
|
|
29
34
|
private handleKrispNoiseFilterDisable;
|
|
35
|
+
/**
|
|
36
|
+
* registers the krisp feature listeners on the given processed track, moving them over from
|
|
37
|
+
* a previously observed one. Passing `undefined` just removes the existing listeners.
|
|
38
|
+
*/
|
|
39
|
+
private observeProcessedTrack;
|
|
40
|
+
/**
|
|
41
|
+
* Sets a processor on this track.
|
|
42
|
+
*
|
|
43
|
+
* The processor is destroyed when the track is stopped, and also when the audio context goes
|
|
44
|
+
* away, so a disconnect ends it on both counts and it has to be set again after reconnecting.
|
|
45
|
+
* See {@link TrackProcessor} for the full lifecycle.
|
|
46
|
+
*/
|
|
30
47
|
setProcessor(processor: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>): Promise<void>;
|
|
48
|
+
protected onMediaStreamTrackChanged(): Promise<void>;
|
|
49
|
+
protected internalStopProcessor(keepElement?: boolean): Promise<void>;
|
|
50
|
+
stop(): void;
|
|
31
51
|
/**
|
|
32
52
|
* @internal
|
|
33
53
|
* @experimental
|
|
54
|
+
*
|
|
55
|
+
* A processor's nodes (`AudioWorkletNode`, ...) can't move between contexts, so the processor
|
|
56
|
+
* is rebuilt whenever the context changes and torn down when there is none left to run in.
|
|
34
57
|
*/
|
|
35
|
-
setAudioContext(audioContext: AudioContext | undefined): void
|
|
58
|
+
setAudioContext(audioContext: AudioContext | undefined): Promise<void>;
|
|
36
59
|
getSenderStats(): Promise<AudioSenderStats | undefined>;
|
|
37
60
|
checkForSilence(): Promise<boolean>;
|
|
38
61
|
}
|
|
@@ -41,6 +41,7 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
|
|
41
41
|
private _isUpstreamPaused;
|
|
42
42
|
get isUpstreamPaused(): boolean;
|
|
43
43
|
get isUserProvided(): boolean;
|
|
44
|
+
get isStopped(): boolean;
|
|
44
45
|
get mediaStreamTrack(): MediaStreamTrack;
|
|
45
46
|
get isLocal(): boolean;
|
|
46
47
|
/**
|
|
@@ -49,6 +50,12 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
|
|
49
50
|
*/
|
|
50
51
|
getSourceTrackSettings(): MediaTrackSettings;
|
|
51
52
|
private setMediaStreamTrack;
|
|
53
|
+
/**
|
|
54
|
+
* Hook invoked after the underlying MediaStreamTrack has been swapped and the processor has
|
|
55
|
+
* been restarted around it, so `processor.processedTrack` already holds the new track.
|
|
56
|
+
* Unlike `onSenderTrackSwapped` this also fires on the restart and unmute paths.
|
|
57
|
+
*/
|
|
58
|
+
protected onMediaStreamTrackChanged(): Promise<void>;
|
|
52
59
|
waitForDimensions(timeout?: number): Promise<Track.Dimensions>;
|
|
53
60
|
setDeviceId(deviceId: ConstrainDOMString): Promise<boolean>;
|
|
54
61
|
abstract restartTrack(constraints?: unknown): Promise<void>;
|
|
@@ -95,16 +102,27 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
|
|
95
102
|
* Sets a processor on this track.
|
|
96
103
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
97
104
|
*
|
|
105
|
+
* The processor lives on the track, so stopping the track destroys it. Unpublishing and
|
|
106
|
+
* disconnecting both stop the track by default, which means a processor has to be set again on
|
|
107
|
+
* the track published after a reconnect. See {@link TrackProcessor} for the full lifecycle,
|
|
108
|
+
* including what `stopLocalTrackOnUnpublish: false` changes.
|
|
109
|
+
*
|
|
98
110
|
* @param processor
|
|
99
111
|
* @param showProcessedStreamLocally
|
|
100
112
|
* @returns
|
|
101
113
|
*/
|
|
102
114
|
setProcessor(processor: TrackProcessor<TrackKind>, showProcessedStreamLocally?: boolean): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Returns the processor currently set on this track, or `undefined` if there is none. A track
|
|
117
|
+
* that was stopped by an unpublish or a disconnect has none. See {@link TrackProcessor}.
|
|
118
|
+
*/
|
|
103
119
|
getProcessor(): TrackProcessor<TrackKind, any> | undefined;
|
|
104
120
|
/**
|
|
105
121
|
* Stops the track processor
|
|
106
122
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
107
123
|
*
|
|
124
|
+
* Only needed to remove a processor from a track that stays published. Unpublishing or
|
|
125
|
+
* disconnecting already destroys it. See {@link TrackProcessor}.
|
|
108
126
|
*/
|
|
109
127
|
stopProcessor(keepElement?: boolean): Promise<void>;
|
|
110
128
|
/**
|
|
@@ -61,6 +61,13 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
|
61
61
|
*/
|
|
62
62
|
private refreshSenderEncodings;
|
|
63
63
|
private applyEncodingsToSender;
|
|
64
|
+
/**
|
|
65
|
+
* Sets a processor on this track, and wires its processed track into the simulcast codecs.
|
|
66
|
+
*
|
|
67
|
+
* The processor is destroyed when the track is stopped, which unpublishing and disconnecting
|
|
68
|
+
* both do by default, so it has to be set again after reconnecting. See {@link TrackProcessor}
|
|
69
|
+
* for the full lifecycle.
|
|
70
|
+
*/
|
|
64
71
|
setProcessor(processor: TrackProcessor<Track.Kind.Video>, showProcessedStreamLocally?: boolean): Promise<void>;
|
|
65
72
|
setDegradationPreference(preference: RTCDegradationPreference): Promise<void>;
|
|
66
73
|
/**
|
|
@@ -10,6 +10,12 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
|
|
|
10
10
|
private gainNode?;
|
|
11
11
|
private sourceNode?;
|
|
12
12
|
private webAudioPluginNodes;
|
|
13
|
+
/**
|
|
14
|
+
* the nodes of the graph currently wired up, in order, so teardown can undo exactly the edges
|
|
15
|
+
* `connectWebAudio` created. Reading `webAudioPluginNodes` instead would miss the old plugins
|
|
16
|
+
* once `setWebAudioPlugins` has replaced them.
|
|
17
|
+
*/
|
|
18
|
+
private connectedNodes;
|
|
13
19
|
private sinkId?;
|
|
14
20
|
constructor(mediaTrack: MediaStreamTrack, sid: string, receiver: RTCRtpReceiver, audioContext?: AudioContext, audioOutput?: AudioOutputOptions, loggerOptions?: LoggerOptions);
|
|
15
21
|
/**
|
|
@@ -19,6 +19,17 @@ export interface AudioProcessorOptions extends ProcessorOptions<Track.Kind.Audio
|
|
|
19
19
|
*/
|
|
20
20
|
export interface VideoProcessorOptions extends ProcessorOptions<Track.Kind.Video> {
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A processor is bound to the track it was set on, not to the room, and it does not survive the
|
|
24
|
+
* track being stopped: unpublishing a track or disconnecting from a room destroys its processor.
|
|
25
|
+
* Both paths stop the track by default, so after reconnecting or republishing, call
|
|
26
|
+
* `setProcessor` again on the new track to put the processor back.
|
|
27
|
+
*
|
|
28
|
+
* The exception is `stopLocalTrackOnUnpublish: false`, which keeps the track alive across an
|
|
29
|
+
* unpublish or disconnect. A video processor then survives with it. An audio processor only
|
|
30
|
+
* survives if the audio context came from the application, because a context the SDK created is
|
|
31
|
+
* closed on disconnect and the processor goes with it.
|
|
32
|
+
*/
|
|
22
33
|
export interface TrackProcessor<T extends Track.Kind, U extends ProcessorOptions<T> = ProcessorOptions<T>> {
|
|
23
34
|
name: string;
|
|
24
35
|
init: (opts: U) => Promise<void>;
|
|
@@ -137,8 +137,19 @@ export interface ObservableMediaElement extends HTMLMediaElement {
|
|
|
137
137
|
export declare function getClientInfo(capabilities?: ClientInfo_Capability[]): ClientInfo;
|
|
138
138
|
export declare function getEmptyVideoStreamTrack(): MediaStreamTrack;
|
|
139
139
|
export declare function createDummyVideoStreamTrack(width?: number, height?: number, enabled?: boolean, paintContent?: boolean): MediaStreamTrack;
|
|
140
|
+
/**
|
|
141
|
+
* Returns a silent audio track, cloned off a shared `AudioContext` created on first use. Hand the
|
|
142
|
+
* clone back to `releaseEmptyAudioStreamTrack` rather than stopping it, so the context can close.
|
|
143
|
+
* A clone that is only stopped stays counted as outstanding, which holds the shared context open
|
|
144
|
+
* for the lifetime of the page.
|
|
145
|
+
*/
|
|
140
146
|
export declare function getEmptyAudioStreamTrack(): MediaStreamTrack;
|
|
141
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Stops a track obtained from `getEmptyAudioStreamTrack` and closes the shared `AudioContext`
|
|
149
|
+
* backing it once every track it handed out has been released. Does nothing for a track this
|
|
150
|
+
* module never handed out, or one that has been released already.
|
|
151
|
+
*/
|
|
152
|
+
export declare function releaseEmptyAudioStreamTrack(track: MediaStreamTrack): Promise<void>;
|
|
142
153
|
/** An object that represents a serialized version of a `new Promise((resolve, reject) => {})`
|
|
143
154
|
* constructor. Wait for a promise resolution with `await future.promise` and explicitly resolve or
|
|
144
155
|
* reject the inner promise with `future.resolve(...)` or `future.reject(...)`.
|
|
@@ -190,6 +201,22 @@ export declare function isAudioCodec(maybeCodec: string): maybeCodec is AudioCod
|
|
|
190
201
|
export declare function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec;
|
|
191
202
|
export declare function unwrapConstraint(constraint: ConstrainDOMString): string;
|
|
192
203
|
export declare function unwrapConstraint(constraint: ConstrainULong): number;
|
|
204
|
+
/**
|
|
205
|
+
* The getUserMedia rejections that no softer constraint can recover from, because the user or the
|
|
206
|
+
* document refused capture outright. Retrying one of these would put a second prompt in front of
|
|
207
|
+
* someone who already said no. The last two names are legacy spellings still reported by older
|
|
208
|
+
* engines. Matched by `name` rather than with `instanceof`, because these are not reliably distinct
|
|
209
|
+
* constructors across environments. happy-dom, which the tests run under, aliases them to `Event`.
|
|
210
|
+
*/
|
|
211
|
+
export declare function isPermissionDeniedError(e: unknown): boolean;
|
|
212
|
+
/** Records that `e` is the rejection getUserMedia itself produced. */
|
|
213
|
+
export declare function markDeviceAcquisitionFailure(e: unknown): void;
|
|
214
|
+
/**
|
|
215
|
+
* Whether a track restart failed at getUserMedia rather than in the work that adopts the new track.
|
|
216
|
+
* Recorded on the side rather than by wrapping, because callers of `restartTrack` match on the
|
|
217
|
+
* rejection's own name.
|
|
218
|
+
*/
|
|
219
|
+
export declare function isDeviceAcquisitionFailure(e: unknown): boolean;
|
|
193
220
|
export declare function toWebsocketUrl(url: string): string;
|
|
194
221
|
export declare function toHttpUrl(url: string): string;
|
|
195
222
|
export declare function extractTranscriptionSegments(transcription: TranscriptionModel, firstReceivedTimesMap: Map<string, number>): TranscriptionSegment[];
|
package/package.json
CHANGED
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
WrappedJoinRequest_Compression,
|
|
12
12
|
} from '@livekit/protocol';
|
|
13
13
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import type { Mutex } from '@livekit/mutex';
|
|
14
15
|
import { ConnectionError, ConnectionErrorReason } from '../room/errors';
|
|
15
16
|
import CriticalTimers from '../room/timers';
|
|
17
|
+
import { Future } from '../room/utils';
|
|
16
18
|
import { SignalClient, SignalConnectionState } from './SignalClient';
|
|
17
19
|
import type { WebSocketCloseInfo, WebSocketConnection } from './WebSocketStream';
|
|
18
20
|
import { WebSocketStream } from './WebSocketStream';
|
|
@@ -147,6 +149,167 @@ describe('SignalClient.connect', () => {
|
|
|
147
149
|
return JoinRequest.fromBinary(bytes);
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
describe('Cancelled connection attempts', () => {
|
|
153
|
+
it.each(['before join', 'while waiting for the connection lock'])(
|
|
154
|
+
'does not open a transport when aborted %s',
|
|
155
|
+
async (when) => {
|
|
156
|
+
const abortController = new AbortController();
|
|
157
|
+
const internals = signalClient as unknown as { connectionLock: Mutex };
|
|
158
|
+
const unlock = await internals.connectionLock.lock();
|
|
159
|
+
mockWebSocketStream({
|
|
160
|
+
connection: createMockConnection(
|
|
161
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
162
|
+
),
|
|
163
|
+
});
|
|
164
|
+
if (when === 'before join') abortController.abort();
|
|
165
|
+
const joining = signalClient.join(
|
|
166
|
+
'wss://test.livekit.io',
|
|
167
|
+
'test-token',
|
|
168
|
+
defaultOptions,
|
|
169
|
+
abortController.signal,
|
|
170
|
+
);
|
|
171
|
+
abortController.abort();
|
|
172
|
+
unlock();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await expect(joining).rejects.toMatchObject({ reason: ConnectionErrorReason.Cancelled });
|
|
176
|
+
expect(WebSocketStream).not.toHaveBeenCalled();
|
|
177
|
+
} finally {
|
|
178
|
+
await signalClient.close();
|
|
179
|
+
vi.mocked(WebSocketStream).mockReset();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it('does not replace a transport when aborted during its teardown', async () => {
|
|
185
|
+
const abortController = new AbortController();
|
|
186
|
+
const internals = signalClient as unknown as {
|
|
187
|
+
connectionLock: Mutex;
|
|
188
|
+
teardownTransport: (reason: string) => Promise<void>;
|
|
189
|
+
};
|
|
190
|
+
const teardownStarted = new Future<void, Error>();
|
|
191
|
+
const finishTeardown = new Future<void, Error>();
|
|
192
|
+
vi.spyOn(internals, 'teardownTransport').mockImplementationOnce(async () => {
|
|
193
|
+
teardownStarted.resolve?.();
|
|
194
|
+
await finishTeardown.promise;
|
|
195
|
+
});
|
|
196
|
+
signalClient.ws = {
|
|
197
|
+
close: () => {},
|
|
198
|
+
closed: Promise.resolve({ closeCode: 1000, reason: '' }),
|
|
199
|
+
} as WebSocketStream;
|
|
200
|
+
mockWebSocketStream({
|
|
201
|
+
connection: createMockConnection(
|
|
202
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
203
|
+
),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const joining = signalClient.join(
|
|
207
|
+
'wss://test.livekit.io',
|
|
208
|
+
'test-token',
|
|
209
|
+
defaultOptions,
|
|
210
|
+
abortController.signal,
|
|
211
|
+
);
|
|
212
|
+
const cancelled = expect(joining).rejects.toMatchObject({
|
|
213
|
+
reason: ConnectionErrorReason.Cancelled,
|
|
214
|
+
});
|
|
215
|
+
await teardownStarted.promise;
|
|
216
|
+
abortController.abort();
|
|
217
|
+
finishTeardown.resolve?.();
|
|
218
|
+
await cancelled;
|
|
219
|
+
// The rejection can settle before the transport replacement resumes.
|
|
220
|
+
const unlock = await internals.connectionLock.lock();
|
|
221
|
+
unlock();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
expect(WebSocketStream).not.toHaveBeenCalled();
|
|
225
|
+
} finally {
|
|
226
|
+
await signalClient.close();
|
|
227
|
+
vi.mocked(WebSocketStream).mockReset();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('does not validate the url when aborted while the websocket is opening', async () => {
|
|
231
|
+
const abortController = new AbortController();
|
|
232
|
+
const internals = signalClient as unknown as { connectionLock: Mutex };
|
|
233
|
+
const opened = new Future<WebSocketConnection, Error>();
|
|
234
|
+
opened.promise.catch(() => {});
|
|
235
|
+
mockWebSocketStream({ opened: opened.promise, readyState: 0 });
|
|
236
|
+
vi.mocked(global.fetch).mockClear();
|
|
237
|
+
|
|
238
|
+
const joining = signalClient.join(
|
|
239
|
+
'wss://test.livekit.io',
|
|
240
|
+
'test-token',
|
|
241
|
+
defaultOptions,
|
|
242
|
+
abortController.signal,
|
|
243
|
+
);
|
|
244
|
+
const cancelled = expect(joining).rejects.toMatchObject({
|
|
245
|
+
reason: ConnectionErrorReason.Cancelled,
|
|
246
|
+
});
|
|
247
|
+
await vi.waitFor(() => expect(WebSocketStream).toHaveBeenCalled());
|
|
248
|
+
abortController.abort();
|
|
249
|
+
opened.reject?.(new Error('closed by abort'));
|
|
250
|
+
await cancelled;
|
|
251
|
+
// let the attempt's continuation run to completion
|
|
252
|
+
const unlock = await internals.connectionLock.lock();
|
|
253
|
+
unlock();
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
257
|
+
} finally {
|
|
258
|
+
await signalClient.close();
|
|
259
|
+
vi.mocked(WebSocketStream).mockReset();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('does not report a join that arrives after the attempt was aborted', async () => {
|
|
264
|
+
const abortController = new AbortController();
|
|
265
|
+
const internals = signalClient as unknown as { connectionLock: Mutex };
|
|
266
|
+
let enqueue: ((chunk: ArrayBuffer) => void) | undefined;
|
|
267
|
+
const readable = new ReadableStream<ArrayBuffer>({
|
|
268
|
+
start(controller) {
|
|
269
|
+
enqueue = (chunk) => controller.enqueue(chunk);
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const writerReady = new Future<void, Error>();
|
|
273
|
+
const writable = new WritableStream();
|
|
274
|
+
const getWriter = writable.getWriter.bind(writable);
|
|
275
|
+
writable.getWriter = () => {
|
|
276
|
+
const writer = getWriter();
|
|
277
|
+
writerReady.resolve?.();
|
|
278
|
+
return writer;
|
|
279
|
+
};
|
|
280
|
+
mockWebSocketStream({
|
|
281
|
+
connection: { readable, writable, protocol: '', extensions: '' },
|
|
282
|
+
});
|
|
283
|
+
const onJoined = vi.fn();
|
|
284
|
+
signalClient.onJoined = onJoined;
|
|
285
|
+
|
|
286
|
+
const joining = signalClient.join(
|
|
287
|
+
'wss://test.livekit.io',
|
|
288
|
+
'test-token',
|
|
289
|
+
defaultOptions,
|
|
290
|
+
abortController.signal,
|
|
291
|
+
);
|
|
292
|
+
const cancelled = expect(joining).rejects.toMatchObject({
|
|
293
|
+
reason: ConnectionErrorReason.Cancelled,
|
|
294
|
+
});
|
|
295
|
+
await writerReady.promise;
|
|
296
|
+
abortController.abort();
|
|
297
|
+
enqueue?.(
|
|
298
|
+
createSignalResponse('join', createJoinResponse()).toBinary().buffer as ArrayBuffer,
|
|
299
|
+
);
|
|
300
|
+
await cancelled;
|
|
301
|
+
const unlock = await internals.connectionLock.lock();
|
|
302
|
+
unlock();
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
expect(onJoined).not.toHaveBeenCalled();
|
|
306
|
+
} finally {
|
|
307
|
+
await signalClient.close();
|
|
308
|
+
vi.mocked(WebSocketStream).mockReset();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
150
313
|
describe('Happy Path - Initial Join', () => {
|
|
151
314
|
it('should successfully connect and receive join response', async () => {
|
|
152
315
|
const joinResponse = createJoinResponse();
|
|
@@ -162,6 +325,17 @@ describe('SignalClient.connect', () => {
|
|
|
162
325
|
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
163
326
|
});
|
|
164
327
|
|
|
328
|
+
it('releases the connection lock when the url is malformed', async () => {
|
|
329
|
+
await expect(signalClient.join('not a url', 'test-token', defaultOptions)).rejects.toThrow();
|
|
330
|
+
|
|
331
|
+
const joinResponse = createJoinResponse();
|
|
332
|
+
const mockReadable = createMockReadableStream([createSignalResponse('join', joinResponse)]);
|
|
333
|
+
mockWebSocketStream({ connection: createMockConnection(mockReadable) });
|
|
334
|
+
|
|
335
|
+
const result = await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
336
|
+
expect(result).toEqual(joinResponse);
|
|
337
|
+
});
|
|
338
|
+
|
|
165
339
|
it('does not advertise packet trailer capability by default', async () => {
|
|
166
340
|
const joinResponse = createJoinResponse();
|
|
167
341
|
const signalResponse = createSignalResponse('join', joinResponse);
|
|
@@ -893,6 +1067,66 @@ describe('SignalClient.connect', () => {
|
|
|
893
1067
|
});
|
|
894
1068
|
});
|
|
895
1069
|
|
|
1070
|
+
describe('Failure Case - Validate Request Hangs', () => {
|
|
1071
|
+
// A socket that never opens on its own (a lossy network) and whose `opened` rejects once the
|
|
1072
|
+
// client closes it, like a real WebSocketStream aborted during the handshake.
|
|
1073
|
+
function mockHangingWebSocket() {
|
|
1074
|
+
vi.mocked(WebSocketStream).mockImplementationOnce(function () {
|
|
1075
|
+
let rejectOpened!: (e: unknown) => void;
|
|
1076
|
+
let resolveClosed!: (info: WebSocketCloseInfo) => void;
|
|
1077
|
+
return {
|
|
1078
|
+
url: 'wss://test.livekit.io',
|
|
1079
|
+
opened: new Promise((_, reject) => (rejectOpened = reject)),
|
|
1080
|
+
closed: new Promise((resolve) => (resolveClosed = resolve)),
|
|
1081
|
+
close: vi.fn(() => {
|
|
1082
|
+
rejectOpened(new Error('aborted during handshake'));
|
|
1083
|
+
resolveClosed({ closeCode: 1006, reason: '' });
|
|
1084
|
+
}),
|
|
1085
|
+
readyState: 0,
|
|
1086
|
+
} as any;
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
it('does not let a timed-out attempt block the next one', async () => {
|
|
1091
|
+
// A validate request on a dead pooled connection only settles once its abort signal fires.
|
|
1092
|
+
// Without a deadline it held the connection lock, so the next attempt never started. A
|
|
1093
|
+
// timed-out attempt now skips validation entirely, since its caller has already been rejected.
|
|
1094
|
+
vi.useFakeTimers();
|
|
1095
|
+
try {
|
|
1096
|
+
const hangingFetch = (_url: unknown, init?: RequestInit) =>
|
|
1097
|
+
new Promise<Response>((_, reject) => {
|
|
1098
|
+
init?.signal?.addEventListener('abort', () => reject(init.signal!.reason));
|
|
1099
|
+
});
|
|
1100
|
+
vi.mocked(fetch).mockImplementationOnce(hangingFetch).mockImplementationOnce(hangingFetch);
|
|
1101
|
+
mockHangingWebSocket();
|
|
1102
|
+
mockHangingWebSocket();
|
|
1103
|
+
|
|
1104
|
+
// building the join request finishes on real I/O, so wait for each socket before advancing
|
|
1105
|
+
const first = expect(
|
|
1106
|
+
signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions),
|
|
1107
|
+
).rejects.toMatchObject({ reason: ConnectionErrorReason.Timeout });
|
|
1108
|
+
await vi.waitFor(() => expect(WebSocketStream).toHaveBeenCalledTimes(1));
|
|
1109
|
+
await vi.advanceTimersByTimeAsync(defaultOptions.websocketTimeout);
|
|
1110
|
+
await first;
|
|
1111
|
+
|
|
1112
|
+
const second = expect(
|
|
1113
|
+
signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions),
|
|
1114
|
+
).rejects.toMatchObject({ reason: ConnectionErrorReason.Timeout });
|
|
1115
|
+
// past the validate deadline, which is what releases the lock the first attempt holds
|
|
1116
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
1117
|
+
await vi.waitFor(() => expect(WebSocketStream).toHaveBeenCalledTimes(2));
|
|
1118
|
+
await vi.advanceTimersByTimeAsync(defaultOptions.websocketTimeout);
|
|
1119
|
+
await second;
|
|
1120
|
+
|
|
1121
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
1122
|
+
} finally {
|
|
1123
|
+
vi.useRealTimers();
|
|
1124
|
+
// drop the unused hanging fetches so they don't leak into later tests
|
|
1125
|
+
vi.mocked(fetch).mockReset();
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
|
|
896
1130
|
describe('Establishing over an existing session', () => {
|
|
897
1131
|
async function joinSuccessfully() {
|
|
898
1132
|
mockWebSocketStream({
|