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
package/src/room/track/Track.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
|
|
|
9
9
|
import type TypedEventEmitter from 'typed-emitter';
|
|
10
10
|
import type { SignalClient } from '../../api/SignalClient';
|
|
11
11
|
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
|
12
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
12
13
|
import { TrackEvent } from '../events';
|
|
13
14
|
import type { LoggerOptions } from '../types';
|
|
14
15
|
import { isFireFox, isSafari, isWeb } from '../utils';
|
|
@@ -59,6 +60,9 @@ export abstract class Track<
|
|
|
59
60
|
|
|
60
61
|
/** @internal */
|
|
61
62
|
setStreamState(value: Track.StreamState) {
|
|
63
|
+
if (this._streamState !== value) {
|
|
64
|
+
this.log.debug(`stream state changed: ${this._streamState} -> ${value}`);
|
|
65
|
+
}
|
|
62
66
|
this._streamState = value;
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -89,8 +93,8 @@ export abstract class Track<
|
|
|
89
93
|
loggerOptions: LoggerOptions = {},
|
|
90
94
|
) {
|
|
91
95
|
super();
|
|
92
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Track);
|
|
93
96
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
97
|
+
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Track, () => this.logContext);
|
|
94
98
|
|
|
95
99
|
this.setMaxListeners(100);
|
|
96
100
|
this.kind = kind;
|
|
@@ -184,11 +188,11 @@ export abstract class Track<
|
|
|
184
188
|
this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
|
|
185
189
|
} else if (e.name === 'AbortError') {
|
|
186
190
|
// commonly triggered by another `play` request, only log for debugging purposes
|
|
187
|
-
log.debug(
|
|
191
|
+
this.log.debug(
|
|
188
192
|
`${hasAudio ? 'audio' : 'video'} playback aborted, likely due to new play request`,
|
|
189
193
|
);
|
|
190
194
|
} else {
|
|
191
|
-
log.warn(`could not playback ${hasAudio ? 'audio' : 'video'}`, e);
|
|
195
|
+
this.log.warn(`could not playback ${hasAudio ? 'audio' : 'video'}`, { error: e });
|
|
192
196
|
}
|
|
193
197
|
// If audio playback isn't allowed make sure we still play back the video
|
|
194
198
|
if (
|
|
@@ -251,6 +255,7 @@ export abstract class Track<
|
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
stop() {
|
|
258
|
+
this.log.debug('stopping track');
|
|
254
259
|
this.stopMonitor();
|
|
255
260
|
this._mediaStreamTrack.stop();
|
|
256
261
|
}
|
|
@@ -278,12 +283,12 @@ export abstract class Track<
|
|
|
278
283
|
|
|
279
284
|
/** @internal */
|
|
280
285
|
updateLoggerOptions(loggerOptions: LoggerOptions) {
|
|
281
|
-
if (loggerOptions.loggerName) {
|
|
282
|
-
this.log = getLogger(loggerOptions.loggerName);
|
|
283
|
-
}
|
|
284
286
|
if (loggerOptions.loggerContextCb) {
|
|
285
287
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
286
288
|
}
|
|
289
|
+
if (loggerOptions.loggerName) {
|
|
290
|
+
this.log = getLogger(loggerOptions.loggerName, () => this.logContext);
|
|
291
|
+
}
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
private recycleElement(element: HTMLMediaElement) {
|
|
@@ -84,7 +84,17 @@ export interface TrackPublishDefaults {
|
|
|
84
84
|
scalabilityMode?: ScalabilityMode;
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Controls how the encoder trades off between resolution and framerate
|
|
88
|
+
* when bandwidth is constrained.
|
|
89
|
+
*
|
|
90
|
+
* - 'maintain-framerate': Prioritizes framerate, reduces resolution if needed
|
|
91
|
+
* - 'maintain-resolution': Prioritizes resolution, drops frames if needed
|
|
92
|
+
* - 'balanced': Balances between both
|
|
93
|
+
*
|
|
94
|
+
* If not set, the SDK uses defaults based on track source:
|
|
95
|
+
* - Camera: 'maintain-framerate' (smoother video for real-time communication)
|
|
96
|
+
* - Screen share: 'maintain-resolution' (clarity is critical for text/UI)
|
|
97
|
+
* - Other/unknown: 'balanced'
|
|
88
98
|
*/
|
|
89
99
|
degradationPreference?: RTCDegradationPreference;
|
|
90
100
|
|
package/src/room/track/record.ts
CHANGED
package/src/room/utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Transcription as TranscriptionModel,
|
|
8
8
|
} from '@livekit/protocol';
|
|
9
9
|
import { type Throws } from '@livekit/throws-transformer/throws';
|
|
10
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
10
11
|
import TypedPromise from '../utils/TypedPromise';
|
|
11
12
|
import { getBrowser } from '../utils/browserParser';
|
|
12
13
|
import type { BrowserDetails } from '../utils/browserParser';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { Socket, createServer } from 'node:net';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createToken } from './signalToken';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* vitest globalSetup for the browser signal e2e suite. It provides the tests a
|
|
9
|
+
* ws:// base URL for the LiveKit mock test-server, via one of two paths:
|
|
10
|
+
*
|
|
11
|
+
* - LK_TEST_SERVER_URL — connect to an already-running server (CI runs the
|
|
12
|
+
* published test-server Docker image as a service; see .github/workflows/e2e.yaml).
|
|
13
|
+
* - LK_SERVER_DIR — a local livekit-server checkout: build cmd/test-server
|
|
14
|
+
* and spawn it on an ephemeral port (dev convenience).
|
|
15
|
+
*
|
|
16
|
+
* If neither is set (or the toolchain is unavailable) the suite is skipped.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HOST = '127.0.0.1';
|
|
20
|
+
|
|
21
|
+
let child: ChildProcess | undefined;
|
|
22
|
+
|
|
23
|
+
function getFreePort(): Promise<number> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const srv = createServer();
|
|
26
|
+
srv.unref();
|
|
27
|
+
srv.on('error', reject);
|
|
28
|
+
srv.listen(0, HOST, () => {
|
|
29
|
+
const addr = srv.address();
|
|
30
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
31
|
+
srv.close(() => resolve(port));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Poll the validate endpoint (from Node — no CORS here) until it answers 200.
|
|
37
|
+
async function waitReady(serverUrl: string): Promise<boolean> {
|
|
38
|
+
const token = await createToken({ signal: 'happy' });
|
|
39
|
+
const httpBase = serverUrl.replace(/^ws/, 'http');
|
|
40
|
+
const validateUrl = `${httpBase}/rtc/validate?access_token=${encodeURIComponent(token)}`;
|
|
41
|
+
const deadline = Date.now() + 20_000;
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
try {
|
|
44
|
+
if ((await fetch(validateUrl)).status === 200) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// not up yet
|
|
49
|
+
}
|
|
50
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default async function setup(project: { provide: (name: string, value: unknown) => void }) {
|
|
56
|
+
const provide = (serverUrl: string) => {
|
|
57
|
+
project.provide('serverUrl', serverUrl);
|
|
58
|
+
project.provide('e2eUnavailable', '');
|
|
59
|
+
};
|
|
60
|
+
const skip = (reason: string) => {
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.warn(`\n[e2e] signal-connection e2e suite SKIPPED: ${reason}\n`);
|
|
63
|
+
project.provide('serverUrl', '');
|
|
64
|
+
project.provide('e2eUnavailable', reason);
|
|
65
|
+
return () => {};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// CI / Docker: connect to an externally-managed server.
|
|
69
|
+
const externalUrl = process.env.LK_TEST_SERVER_URL;
|
|
70
|
+
if (externalUrl) {
|
|
71
|
+
if (!(await waitReady(externalUrl))) {
|
|
72
|
+
return skip(`test-server at ${externalUrl} did not become ready`);
|
|
73
|
+
}
|
|
74
|
+
provide(externalUrl);
|
|
75
|
+
return () => {}; // container lifecycle is owned by CI
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Local dev: build + spawn cmd/test-server from a livekit-server checkout.
|
|
79
|
+
const serverDir = process.env.LK_SERVER_DIR;
|
|
80
|
+
if (!serverDir || !existsSync(serverDir)) {
|
|
81
|
+
return skip(
|
|
82
|
+
'set LK_TEST_SERVER_URL (a running server) or LK_SERVER_DIR (a livekit-server checkout)',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const binary = join(serverDir, 'bin', 'test-server');
|
|
87
|
+
if (!process.env.LK_E2E_SKIP_BUILD) {
|
|
88
|
+
try {
|
|
89
|
+
execFileSync('go', ['build', '-o', 'bin/test-server', './cmd/test-server'], {
|
|
90
|
+
cwd: serverDir,
|
|
91
|
+
stdio: 'pipe',
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return skip(`failed to build test-server: ${(e as Error).message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!existsSync(binary)) {
|
|
98
|
+
return skip(`test-server binary missing at ${binary}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fresh ephemeral port each run so a leftover server from a crashed run can
|
|
102
|
+
// never be mistaken for ours (vitest browser-mode teardown is unreliable, so
|
|
103
|
+
// a spawned server may outlive the run; the ephemeral port makes that
|
|
104
|
+
// harmless, at the cost of leaving at most one idle process per run).
|
|
105
|
+
const port = Number(process.env.LK_TEST_SERVER_PORT) || (await getFreePort());
|
|
106
|
+
const serverUrl = `ws://${HOST}:${port}`;
|
|
107
|
+
|
|
108
|
+
child = spawn(binary, ['--ports', String(port), '--bind', HOST], {
|
|
109
|
+
cwd: serverDir,
|
|
110
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
|
+
});
|
|
112
|
+
let serverLog = '';
|
|
113
|
+
child.stdout?.on('data', (d: unknown) => {
|
|
114
|
+
serverLog += String(d);
|
|
115
|
+
});
|
|
116
|
+
child.stderr?.on('data', (d: unknown) => {
|
|
117
|
+
serverLog += String(d);
|
|
118
|
+
});
|
|
119
|
+
// With stdio: 'pipe' these are net.Sockets (typed as Readable, which lacks unref);
|
|
120
|
+
// unref the pipe handles so their 'data' listeners don't keep the parent alive.
|
|
121
|
+
if (child.stdout instanceof Socket) child.stdout.unref();
|
|
122
|
+
if (child.stderr instanceof Socket) child.stderr.unref();
|
|
123
|
+
child.unref();
|
|
124
|
+
|
|
125
|
+
const kill = () => {
|
|
126
|
+
try {
|
|
127
|
+
child?.kill('SIGKILL');
|
|
128
|
+
} catch {
|
|
129
|
+
// already gone
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
process.once('exit', kill);
|
|
133
|
+
process.once('SIGINT', () => {
|
|
134
|
+
kill();
|
|
135
|
+
process.exit(130);
|
|
136
|
+
});
|
|
137
|
+
process.once('SIGTERM', () => {
|
|
138
|
+
kill();
|
|
139
|
+
process.exit(143);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!(await waitReady(serverUrl))) {
|
|
143
|
+
kill();
|
|
144
|
+
return skip(
|
|
145
|
+
`test-server did not become ready on ${serverUrl}\n--- server output ---\n${serverLog}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
provide(serverUrl);
|
|
150
|
+
return kill;
|
|
151
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SignJWT } from 'jose';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mint a LiveKit access token for the mock test-server (HS256, dev secret).
|
|
5
|
+
*
|
|
6
|
+
* The mock's signal behavior mode is selected via a participant attribute
|
|
7
|
+
* under the reserved key `lk.mock` (dot notation, matching LiveKit's internal
|
|
8
|
+
* attribute convention), whose value is the JSON control object
|
|
9
|
+
* `{"signal":"<mode>"}` — mirroring the X-Lk-Mock header protocol. Attributes
|
|
10
|
+
* are a standard AccessToken field (a string map), so a dedicated `lk-mock`
|
|
11
|
+
* key coexists with any real metadata/attributes and needs no bespoke token
|
|
12
|
+
* construction. The room name is just a room; it no longer encodes behavior.
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenOptions {
|
|
15
|
+
/** Mock behavior mode (e.g. 'no_pong'); omitted → the mock defaults to 'happy'. */
|
|
16
|
+
signal?: string;
|
|
17
|
+
/** LeaveRequest action the leave modes should send (0=DISCONNECT,1=RESUME,2=RECONNECT). */
|
|
18
|
+
leaveAction?: number;
|
|
19
|
+
room?: string;
|
|
20
|
+
identity?: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
secret?: string;
|
|
23
|
+
ttlSeconds?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createToken(opts: TokenOptions = {}): Promise<string> {
|
|
27
|
+
const {
|
|
28
|
+
signal,
|
|
29
|
+
leaveAction,
|
|
30
|
+
room = 'e2e-room',
|
|
31
|
+
identity = `test-${Math.random().toString(36).slice(2, 8)}`,
|
|
32
|
+
apiKey = 'devkey',
|
|
33
|
+
secret = 'secret',
|
|
34
|
+
ttlSeconds = 600,
|
|
35
|
+
} = opts;
|
|
36
|
+
const key = new TextEncoder().encode(secret);
|
|
37
|
+
const payload: Record<string, unknown> = {
|
|
38
|
+
video: { room, roomJoin: true, canPublish: true, canSubscribe: true, canPublishData: true },
|
|
39
|
+
};
|
|
40
|
+
if (signal) {
|
|
41
|
+
const control: Record<string, unknown> = { signal };
|
|
42
|
+
if (leaveAction !== undefined) {
|
|
43
|
+
control.leaveAction = leaveAction;
|
|
44
|
+
}
|
|
45
|
+
payload.attributes = { 'lk.mock': JSON.stringify(control) };
|
|
46
|
+
}
|
|
47
|
+
return new SignJWT(payload)
|
|
48
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
49
|
+
.setIssuer(apiKey)
|
|
50
|
+
.setSubject(identity)
|
|
51
|
+
.setIssuedAt()
|
|
52
|
+
.setExpirationTime(`${ttlSeconds}s`)
|
|
53
|
+
.sign(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A syntactically-valid token signed with the WRONG secret — for 401 tests. */
|
|
57
|
+
export async function createInvalidToken(): Promise<string> {
|
|
58
|
+
return createToken({ secret: 'not-the-secret' });
|
|
59
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'vitest';
|
|
2
|
+
|
|
3
|
+
declare module 'vitest' {
|
|
4
|
+
interface ProvidedContext {
|
|
5
|
+
/** ws:// base URL of the spawned mock test-server, or '' when unavailable. */
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
/** Non-empty reason string when the e2e suite must be skipped. */
|
|
8
|
+
e2eUnavailable: string;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -3,4 +3,9 @@
|
|
|
3
3
|
// only accept the non-shared variant `Uint8Array<ArrayBuffer>`. Using `ReturnType<typeof Uint8Array.from>`
|
|
4
4
|
// resolves to that non-shared variant on TS versions that support the generic, while remaining
|
|
5
5
|
// equivalent to plain `Uint8Array` on older versions — so this alias works across the range we support.
|
|
6
|
-
|
|
6
|
+
//
|
|
7
|
+
// Exported as a normal type (rather than an ambient global) so it is emitted into the published `dist`
|
|
8
|
+
// types and imported explicitly wherever it is used. An earlier ambient `.d.ts` declaration was resolved
|
|
9
|
+
// during our own build but never shipped, so consumers' type-checkers (and tools like API Extractor)
|
|
10
|
+
// failed with "Cannot find name 'NonSharedUint8Array'" on our emitted `.d.ts` files.
|
|
11
|
+
export type NonSharedUint8Array = ReturnType<typeof Uint8Array.from>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DataPacketBuffer } from './dataPacketBuffer';
|
|
3
|
+
|
|
4
|
+
const item = (sequence: number, size: number, sent: boolean) => ({
|
|
5
|
+
data: new Uint8Array(size),
|
|
6
|
+
sequence,
|
|
7
|
+
sent,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('DataPacketBuffer', () => {
|
|
11
|
+
it('trims sent packets down to the buffered amount', () => {
|
|
12
|
+
const buffer = new DataPacketBuffer();
|
|
13
|
+
buffer.push(item(1, 100, true));
|
|
14
|
+
buffer.push(item(2, 100, true));
|
|
15
|
+
buffer.push(item(3, 100, true));
|
|
16
|
+
|
|
17
|
+
// 150 buffered bytes cover the last two packets; the first is fully delivered.
|
|
18
|
+
buffer.alignBufferedAmount(150);
|
|
19
|
+
|
|
20
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([2, 3]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('never trims unsent packets, even when the buffered amount is zero', () => {
|
|
24
|
+
const buffer = new DataPacketBuffer();
|
|
25
|
+
buffer.push(item(1, 100, false));
|
|
26
|
+
buffer.push(item(2, 100, false));
|
|
27
|
+
|
|
28
|
+
buffer.alignBufferedAmount(0);
|
|
29
|
+
|
|
30
|
+
expect(buffer.length).toBe(2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does not let unsent tail bytes cause over-trimming of the sent prefix', () => {
|
|
34
|
+
const buffer = new DataPacketBuffer();
|
|
35
|
+
buffer.push(item(1, 100, true));
|
|
36
|
+
buffer.push(item(2, 100, true));
|
|
37
|
+
// Queued during a reconnect window — not part of the channel's bufferedAmount.
|
|
38
|
+
buffer.push(item(3, 100, false));
|
|
39
|
+
buffer.push(item(4, 100, false));
|
|
40
|
+
|
|
41
|
+
// Both sent packets are still in flight; nothing may be trimmed. With size accounting based
|
|
42
|
+
// on the total (400) instead of sent bytes (200), both sent packets would be popped here.
|
|
43
|
+
buffer.alignBufferedAmount(200);
|
|
44
|
+
|
|
45
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([1, 2, 3, 4]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('markSent makes a single packet eligible for trimming and tracks sent size', () => {
|
|
49
|
+
const buffer = new DataPacketBuffer();
|
|
50
|
+
const first = item(1, 100, false);
|
|
51
|
+
const second = item(2, 100, false);
|
|
52
|
+
buffer.push(first);
|
|
53
|
+
buffer.push(second);
|
|
54
|
+
|
|
55
|
+
// Only the marked packet can be trimmed; the still-unsent one blocks the front.
|
|
56
|
+
buffer.markSent(first);
|
|
57
|
+
buffer.alignBufferedAmount(0);
|
|
58
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([1, 2]);
|
|
59
|
+
|
|
60
|
+
buffer.markSent(second);
|
|
61
|
+
buffer.alignBufferedAmount(50);
|
|
62
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([2]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('getUnsent returns only unsent packets, in order', () => {
|
|
66
|
+
const buffer = new DataPacketBuffer();
|
|
67
|
+
const a = item(1, 100, false);
|
|
68
|
+
const b = item(2, 100, false);
|
|
69
|
+
buffer.push(a);
|
|
70
|
+
buffer.push(b);
|
|
71
|
+
buffer.push(item(3, 100, false));
|
|
72
|
+
buffer.markSent(b);
|
|
73
|
+
|
|
74
|
+
expect(buffer.getUnsent().map((i) => i.sequence)).toEqual([1, 3]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('markAllUnsent flags every packet for re-send and resets sent size', () => {
|
|
78
|
+
const buffer = new DataPacketBuffer();
|
|
79
|
+
buffer.push(item(1, 100, true));
|
|
80
|
+
buffer.push(item(2, 100, true));
|
|
81
|
+
|
|
82
|
+
buffer.markAllUnsent();
|
|
83
|
+
expect(buffer.getUnsent().map((i) => i.sequence)).toEqual([1, 2]);
|
|
84
|
+
|
|
85
|
+
// With everything unsent, nothing can be trimmed even at a zero buffered amount.
|
|
86
|
+
buffer.alignBufferedAmount(0);
|
|
87
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([1, 2]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('popToSequence drops acked packets regardless of sent state', () => {
|
|
91
|
+
const buffer = new DataPacketBuffer();
|
|
92
|
+
buffer.push(item(1, 100, true));
|
|
93
|
+
buffer.push(item(2, 100, false));
|
|
94
|
+
buffer.push(item(3, 100, false));
|
|
95
|
+
|
|
96
|
+
buffer.popToSequence(2);
|
|
97
|
+
|
|
98
|
+
expect(buffer.getAll().map((i) => i.sequence)).toEqual([3]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
2
|
+
|
|
1
3
|
export interface DataPacketItem {
|
|
2
4
|
data: NonSharedUint8Array;
|
|
3
5
|
sequence: number;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the packet has been handed to the data channel. Unsent packets are queued for the
|
|
8
|
+
* resume replay (e.g. sends that landed in a reconnect window) and must never be trimmed by
|
|
9
|
+
* {@link DataPacketBuffer.alignBufferedAmount}, which reasons about the channel's buffered
|
|
10
|
+
* bytes — those only ever contain sent packets.
|
|
11
|
+
*/
|
|
12
|
+
sent: boolean;
|
|
4
13
|
}
|
|
5
14
|
|
|
6
15
|
export class DataPacketBuffer {
|
|
@@ -8,15 +17,23 @@ export class DataPacketBuffer {
|
|
|
8
17
|
|
|
9
18
|
private _totalSize = 0;
|
|
10
19
|
|
|
20
|
+
private _sentSize = 0;
|
|
21
|
+
|
|
11
22
|
push(item: DataPacketItem) {
|
|
12
23
|
this.buffer.push(item);
|
|
13
24
|
this._totalSize += item.data.byteLength;
|
|
25
|
+
if (item.sent) {
|
|
26
|
+
this._sentSize += item.data.byteLength;
|
|
27
|
+
}
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
pop(): DataPacketItem | undefined {
|
|
17
31
|
const item = this.buffer.shift();
|
|
18
32
|
if (item) {
|
|
19
33
|
this._totalSize -= item.data.byteLength;
|
|
34
|
+
if (item.sent) {
|
|
35
|
+
this._sentSize -= item.data.byteLength;
|
|
36
|
+
}
|
|
20
37
|
}
|
|
21
38
|
return item;
|
|
22
39
|
}
|
|
@@ -25,6 +42,31 @@ export class DataPacketBuffer {
|
|
|
25
42
|
return this.buffer.slice();
|
|
26
43
|
}
|
|
27
44
|
|
|
45
|
+
/** Every queued packet not yet handed to the channel, in sequence order. */
|
|
46
|
+
getUnsent(): DataPacketItem[] {
|
|
47
|
+
return this.buffer.filter((item) => !item.sent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Marks a single queued packet as handed to the channel. */
|
|
51
|
+
markSent(item: DataPacketItem) {
|
|
52
|
+
if (!item.sent) {
|
|
53
|
+
item.sent = true;
|
|
54
|
+
this._sentSize += item.data.byteLength;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
|
|
60
|
+
* still buffered was sent on the previous channel (or deferred) and must be re-handed to the
|
|
61
|
+
* current one, so none of it counts as sent until the replay actually transmits it.
|
|
62
|
+
*/
|
|
63
|
+
markAllUnsent() {
|
|
64
|
+
for (const item of this.buffer) {
|
|
65
|
+
item.sent = false;
|
|
66
|
+
}
|
|
67
|
+
this._sentSize = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
popToSequence(sequence: number) {
|
|
29
71
|
while (this.buffer.length > 0) {
|
|
30
72
|
const first = this.buffer[0];
|
|
@@ -39,7 +81,12 @@ export class DataPacketBuffer {
|
|
|
39
81
|
alignBufferedAmount(bufferedAmount: number) {
|
|
40
82
|
while (this.buffer.length > 0) {
|
|
41
83
|
const first = this.buffer[0];
|
|
42
|
-
|
|
84
|
+
// Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
|
|
85
|
+
// resume replay — trimming them would silently lose them.
|
|
86
|
+
if (!first.sent) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (this._sentSize - first.data.byteLength <= bufferedAmount) {
|
|
43
90
|
break;
|
|
44
91
|
}
|
|
45
92
|
this.pop();
|