livekit-client 2.21.0 → 2.22.1
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 +10 -10
- 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 +189 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2243 -270
- 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 +189 -2
- 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 +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +51 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +27 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +15 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +51 -3
- package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
- package/dist/ts4.2/room/Room.d.ts +10 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +15 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +19 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +183 -81
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +168 -18
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +122 -22
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
- package/src/room/data-stream/incoming/StreamReader.ts +1 -1
- package/src/room/errors.ts +1 -2
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +29 -1
- package/src/room/utils.test.ts +94 -2
- package/src/room/utils.ts +53 -1
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/utils/machineInspector.ts +90 -0
package/src/room/utils.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function supportsAddTrack() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function supportsAdaptiveStream() {
|
|
61
|
-
return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
|
|
61
|
+
return typeof ResizeObserver !== 'undefined' && typeof IntersectionObserver !== 'undefined';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function supportsDynacast() {
|
|
@@ -146,6 +146,45 @@ export function isSVCCodec(codec?: string): boolean {
|
|
|
146
146
|
return codec === 'av1' || codec === 'vp9';
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
|
|
151
|
+
*
|
|
152
|
+
* Chrome only offers the extension on transceivers that can send, so one we create to receive on
|
|
153
|
+
* never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
|
|
154
|
+
* assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
|
|
155
|
+
* transceiver rather than munging the extension into the SDP leaves the browser owning the
|
|
156
|
+
* extension id, which is what keeps that id consistent across the bundle and across
|
|
157
|
+
* renegotiations.
|
|
158
|
+
*
|
|
159
|
+
* A no-op where the browser offers no such control, or does not know the extension at all.
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
export function negotiateDependencyDescriptor(transceiver: RTCRtpTransceiver): boolean {
|
|
163
|
+
const extensions = transceiver.getHeaderExtensionsToNegotiate?.();
|
|
164
|
+
if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const dd = extensions.find((ext) => ext.uri === ddExtensionURI);
|
|
168
|
+
if (!dd) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (dd.direction !== 'stopped') {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
// sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
|
|
175
|
+
// the form the server already emits where it is the one offering — rather than one carrying a
|
|
176
|
+
// `/recvonly` suffix that its parser may not expect
|
|
177
|
+
dd.direction = 'sendrecv';
|
|
178
|
+
try {
|
|
179
|
+
transceiver.setHeaderExtensionsToNegotiate(extensions);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// a rejected direction throws. Negotiating without the extension is what happened before this
|
|
183
|
+
// existed, so it is not worth failing the connection over
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
149
188
|
export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
|
|
150
189
|
if (!document || isSafariBased()) {
|
|
151
190
|
return false;
|
|
@@ -867,3 +906,16 @@ export function isPublisherOfferWithJoinSupported() {
|
|
|
867
906
|
// we have connectivity issue about publisher offer with join on firefox #1919
|
|
868
907
|
return isCompressionStreamSupported() && !isFireFox();
|
|
869
908
|
}
|
|
909
|
+
|
|
910
|
+
export function extractTrackSid(
|
|
911
|
+
mediaTrack: MediaStreamTrack,
|
|
912
|
+
stream: MediaStream,
|
|
913
|
+
): Track.SID | undefined {
|
|
914
|
+
const [, streamId] = unpackStreamId(stream.id);
|
|
915
|
+
if (streamId?.startsWith('TR')) {
|
|
916
|
+
return streamId;
|
|
917
|
+
}
|
|
918
|
+
if (mediaTrack.id.startsWith('TR')) {
|
|
919
|
+
return mediaTrack.id;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper: resolves `true` if `promise` is still pending after `ms`
|
|
3
|
+
* (default: one macrotask), `false` if it has already settled — regardless of
|
|
4
|
+
* whether it resolved or rejected.
|
|
5
|
+
*
|
|
6
|
+
* Use to assert that something has *not* happened yet (e.g. an onClose callback
|
|
7
|
+
* that must stay silent), optionally within a timeout window. Preferable to
|
|
8
|
+
* `Promise.race([p, Promise.resolve(sentinel)])` when the "not yet" needs to
|
|
9
|
+
* hold for a duration rather than just the current microtask.
|
|
10
|
+
*/
|
|
11
|
+
export async function isPending(promise: Promise<unknown>, ms = 0): Promise<boolean> {
|
|
12
|
+
const pending = Symbol('pending');
|
|
13
|
+
// Swallow settlement (incl. rejection) so racing it can't raise unhandled
|
|
14
|
+
// rejections when the timer wins.
|
|
15
|
+
const settled = promise.then(
|
|
16
|
+
() => 'settled' as const,
|
|
17
|
+
() => 'settled' as const,
|
|
18
|
+
);
|
|
19
|
+
const timer = new Promise<typeof pending>((resolve) => {
|
|
20
|
+
setTimeout(() => resolve(pending), ms);
|
|
21
|
+
});
|
|
22
|
+
return (await Promise.race([settled, timer])) === pending;
|
|
23
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { Socket, createServer } from 'node:net';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { sleep } from '../room/utils';
|
|
5
6
|
import { createToken } from './signalToken';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -47,7 +48,7 @@ async function waitReady(serverUrl: string): Promise<boolean> {
|
|
|
47
48
|
} catch {
|
|
48
49
|
// not up yet
|
|
49
50
|
}
|
|
50
|
-
await
|
|
51
|
+
await sleep(200);
|
|
51
52
|
}
|
|
52
53
|
return false;
|
|
53
54
|
}
|
package/src/test/signalToken.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Mint a LiveKit access token for the mock test-server (HS256, dev secret).
|
|
@@ -33,24 +33,28 @@ export async function createToken(opts: TokenOptions = {}): Promise<string> {
|
|
|
33
33
|
secret = 'secret',
|
|
34
34
|
ttlSeconds = 600,
|
|
35
35
|
} = opts;
|
|
36
|
-
const
|
|
37
|
-
const payload: Record<string, unknown> = {
|
|
38
|
-
video: { room, roomJoin: true, canPublish: true, canSubscribe: true, canPublishData: true },
|
|
39
|
-
};
|
|
36
|
+
const attributes: Record<string, string> = {};
|
|
40
37
|
if (signal) {
|
|
41
38
|
const control: Record<string, unknown> = { signal };
|
|
42
39
|
if (leaveAction !== undefined) {
|
|
43
40
|
control.leaveAction = leaveAction;
|
|
44
41
|
}
|
|
45
|
-
|
|
42
|
+
attributes['lk.mock'] = JSON.stringify(control);
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const token = new AccessToken(apiKey, secret, {
|
|
45
|
+
ttl: ttlSeconds,
|
|
46
|
+
identity,
|
|
47
|
+
attributes,
|
|
48
|
+
});
|
|
49
|
+
token.addGrant({
|
|
50
|
+
room,
|
|
51
|
+
roomJoin: true,
|
|
52
|
+
canPublish: true,
|
|
53
|
+
canSubscribe: true,
|
|
54
|
+
canPublishData: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return token.toJwt();
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/** A syntactically-valid token signed with the WRONG secret — for 401 tests. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-transceiver control over which RTP header extensions get negotiated. Declared as optional
|
|
3
|
+
* because it is not in lib.dom yet and not every browser implements it.
|
|
4
|
+
* https://w3c.github.io/webrtc-extensions/#rtcrtptransceiver-interface-extensions
|
|
5
|
+
*/
|
|
6
|
+
interface RTCRtpTransceiver {
|
|
7
|
+
getHeaderExtensionsToNegotiate?(): RTCRtpHeaderExtensionCapability[];
|
|
8
|
+
setHeaderExtensionsToNegotiate?(extensions: RTCRtpHeaderExtensionCapability[]): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RTCRtpHeaderExtensionCapability {
|
|
12
|
+
direction?: 'sendrecv' | 'sendonly' | 'recvonly' | 'stopped';
|
|
13
|
+
}
|
package/src/utils/AsyncQueue.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in registry that lets a development tool observe the state machines driving the connection
|
|
3
|
+
* layer.
|
|
4
|
+
*
|
|
5
|
+
* The machines are private to the objects that own them and, more importantly, do not outlive them:
|
|
6
|
+
* a full reconnect replaces the whole engine along with its `SignalClient`, so anything watching a
|
|
7
|
+
* single machine reference goes blind exactly when the interesting part starts. Machines therefore
|
|
8
|
+
* announce themselves here as they are constructed, and a subscriber sees the whole succession.
|
|
9
|
+
*
|
|
10
|
+
* Nothing is recorded until {@link enableMachineInspector} is called, which no shipping code does —
|
|
11
|
+
* with the registry off, announcing is a comparison and a return.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The slice of machina's `Fsm` surface an inspector needs. Structural rather than machina's own
|
|
16
|
+
* type so that any machine satisfies it regardless of its state and input unions.
|
|
17
|
+
*/
|
|
18
|
+
export interface InspectableMachine {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly initialState: string;
|
|
21
|
+
readonly states: Record<string, Record<string, unknown>>;
|
|
22
|
+
readonly context?: unknown;
|
|
23
|
+
currentState(): string;
|
|
24
|
+
canHandle(input: string): boolean;
|
|
25
|
+
on(eventName: string, callback: (data: any) => void): { off(): void };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MachineAnnouncement {
|
|
29
|
+
/** What the machine drives, e.g. `signal`. Not unique: each new `SignalClient` announces one. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** How many machines have been announced before this one, so successive instances are tellable apart. */
|
|
32
|
+
seq: number;
|
|
33
|
+
/**
|
|
34
|
+
* When the machine was constructed. Carried on the announcement because subscribers receive the
|
|
35
|
+
* recorded ones on subscribe, and stamping those at replay time would date them all to whenever
|
|
36
|
+
* the tool happened to open.
|
|
37
|
+
*/
|
|
38
|
+
at: number;
|
|
39
|
+
machine: InspectableMachine;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Kept small: enough for a panel opened mid-session to see how the connection got where it is. */
|
|
43
|
+
const HISTORY_LIMIT = 16;
|
|
44
|
+
|
|
45
|
+
let enabled = false;
|
|
46
|
+
let announced: MachineAnnouncement[] = [];
|
|
47
|
+
let subscribers: Array<(announcement: MachineAnnouncement) => void> = [];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Starts recording machine announcements. Call before the machines of interest are constructed —
|
|
51
|
+
* for the signal machine that means before the `Room` is created.
|
|
52
|
+
*/
|
|
53
|
+
export function enableMachineInspector() {
|
|
54
|
+
enabled = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isMachineInspectorEnabled() {
|
|
58
|
+
return enabled;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
|
|
62
|
+
export function announceMachine(label: string, machine: InspectableMachine) {
|
|
63
|
+
if (!enabled) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const announcement: MachineAnnouncement = {
|
|
67
|
+
label,
|
|
68
|
+
seq: announced.length,
|
|
69
|
+
at: Date.now(),
|
|
70
|
+
machine,
|
|
71
|
+
};
|
|
72
|
+
announced = [...announced.slice(-(HISTORY_LIMIT - 1)), announcement];
|
|
73
|
+
for (const subscriber of subscribers) {
|
|
74
|
+
subscriber(announcement);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to machine announcements, replaying the ones already recorded so that a panel opened
|
|
80
|
+
* after connecting still sees the machine currently in charge. Returns an unsubscribe function.
|
|
81
|
+
*/
|
|
82
|
+
export function onMachineAnnounced(callback: (announcement: MachineAnnouncement) => void) {
|
|
83
|
+
subscribers = [...subscribers, callback];
|
|
84
|
+
for (const announcement of announced) {
|
|
85
|
+
callback(announcement);
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
subscribers = subscribers.filter((subscriber) => subscriber !== callback);
|
|
89
|
+
};
|
|
90
|
+
}
|