@livekit/rtc-node 0.13.33 → 0.13.34
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/{audio_stream-CnODFAmo.d.cts → audio_stream-BNVaONuH.d.cts} +36 -11
- package/dist/audio_stream.d.cts +2 -2
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/stream_reader.cjs +97 -58
- package/dist/data_streams/stream_reader.cjs.map +1 -1
- package/dist/data_streams/stream_reader.d.cts +1 -1
- package/dist/data_streams/stream_reader.d.ts +28 -11
- package/dist/data_streams/stream_reader.d.ts.map +1 -1
- package/dist/data_streams/stream_writer.d.cts +1 -1
- package/dist/data_streams/types.cjs.map +1 -1
- package/dist/data_streams/types.d.cts +1 -1
- package/dist/data_streams/types.d.ts +6 -0
- package/dist/data_streams/types.d.ts.map +1 -1
- package/dist/e2ee.cjs +5 -1
- package/dist/e2ee.cjs.map +1 -1
- package/dist/e2ee.d.cts +3 -1
- package/dist/e2ee.d.ts +3 -1
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/participant.cjs +178 -196
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -2
- package/dist/participant.d.ts +22 -5
- package/dist/participant.d.ts.map +1 -1
- package/dist/room.cjs +166 -126
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +3 -3
- package/dist/room.d.ts +16 -8
- package/dist/room.d.ts.map +1 -1
- package/dist/tests/e2e_common.cjs +157 -0
- package/dist/tests/e2e_common.cjs.map +1 -0
- package/dist/tests/e2e_common.d.cts +56 -0
- package/dist/tests/e2e_common.d.ts +37 -0
- package/dist/tests/e2e_common.d.ts.map +1 -0
- package/dist/track.d.cts +2 -2
- package/dist/track_publication.d.cts +2 -2
- package/dist/{types-_PdPVish.d.cts → types-f7wTlFjs.d.cts} +34 -11
- package/dist/utils.cjs +27 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +8 -1
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/video_stream.d.cts +2 -2
- package/package.json +2 -2
- package/src/audio_stream_room_lifecycle.test.ts +4 -1
- package/src/data_streams/stream_reader.ts +132 -82
- package/src/data_streams/types.ts +6 -0
- package/src/e2ee.ts +7 -0
- package/src/participant.ts +251 -228
- package/src/room.ts +233 -144
- package/src/tests/e2e.test.ts +50 -139
- package/src/tests/e2e_common.ts +163 -0
- package/src/tests/e2e_data_streams.test.ts +639 -0
- package/src/utils.ts +30 -0
- package/src/version.ts +1 -1
package/src/tests/e2e.test.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import { AccessToken } from 'livekit-server-sdk';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
-
import { afterAll,
|
|
5
|
+
import { afterAll, expect, it as itRaw } from 'vitest';
|
|
8
6
|
import {
|
|
9
7
|
AudioFrame,
|
|
10
8
|
AudioSource,
|
|
@@ -12,7 +10,6 @@ import {
|
|
|
12
10
|
ConnectionState,
|
|
13
11
|
LocalAudioTrack,
|
|
14
12
|
ParticipantKind,
|
|
15
|
-
Room,
|
|
16
13
|
RoomEvent,
|
|
17
14
|
RpcError,
|
|
18
15
|
SimulateScenarioKind,
|
|
@@ -21,134 +18,19 @@ import {
|
|
|
21
18
|
TrackSource,
|
|
22
19
|
dispose,
|
|
23
20
|
} from '../index.js';
|
|
21
|
+
import {
|
|
22
|
+
concatUint8,
|
|
23
|
+
connectTestRooms,
|
|
24
|
+
describeE2E,
|
|
25
|
+
testTimeoutMs,
|
|
26
|
+
waitFor,
|
|
27
|
+
waitForRoomEvent,
|
|
28
|
+
withTimeout,
|
|
29
|
+
} from './e2e_common.js';
|
|
24
30
|
|
|
25
31
|
// use concurrent testing if available on the runner (currently not supported by bun's api)
|
|
26
32
|
const it = typeof itRaw.concurrent === 'function' ? itRaw.concurrent : itRaw;
|
|
27
33
|
|
|
28
|
-
const hasE2EEnv =
|
|
29
|
-
!!process.env.LIVEKIT_URL && !!process.env.LIVEKIT_API_KEY && !!process.env.LIVEKIT_API_SECRET;
|
|
30
|
-
const describeE2E = hasE2EEnv ? describe : describe.skip;
|
|
31
|
-
const testTimeoutMs = 10_000;
|
|
32
|
-
|
|
33
|
-
type TestEnv = {
|
|
34
|
-
url: string;
|
|
35
|
-
apiKey: string;
|
|
36
|
-
apiSecret: string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function normalizeLiveKitUrl(url: string): string {
|
|
40
|
-
if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
|
|
41
|
-
if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
|
|
42
|
-
return url;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getTestEnv(): TestEnv {
|
|
46
|
-
if (!hasE2EEnv) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
'Missing required env vars for e2e: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET',
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
url: normalizeLiveKitUrl(process.env.LIVEKIT_URL!),
|
|
53
|
-
apiKey: process.env.LIVEKIT_API_KEY!,
|
|
54
|
-
apiSecret: process.env.LIVEKIT_API_SECRET!,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
59
|
-
return await Promise.race([
|
|
60
|
-
promise,
|
|
61
|
-
(async () => {
|
|
62
|
-
await delay(timeoutMs);
|
|
63
|
-
throw new Error(message);
|
|
64
|
-
})(),
|
|
65
|
-
]);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function waitFor(
|
|
69
|
-
condition: () => boolean,
|
|
70
|
-
opts: { timeoutMs: number; intervalMs?: number; debugName?: string },
|
|
71
|
-
): Promise<void> {
|
|
72
|
-
const intervalMs = opts.intervalMs ?? 50;
|
|
73
|
-
const deadline = Date.now() + opts.timeoutMs;
|
|
74
|
-
while (Date.now() < deadline) {
|
|
75
|
-
if (condition()) return;
|
|
76
|
-
await delay(intervalMs);
|
|
77
|
-
}
|
|
78
|
-
throw new Error(`Timed out waiting for condition${opts.debugName ? ` (${opts.debugName})` : ''}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function createJoinToken(params: {
|
|
82
|
-
env: TestEnv;
|
|
83
|
-
roomName: string;
|
|
84
|
-
identity: string;
|
|
85
|
-
name: string;
|
|
86
|
-
}): Promise<string> {
|
|
87
|
-
const token = new AccessToken(params.env.apiKey, params.env.apiSecret, {
|
|
88
|
-
identity: params.identity,
|
|
89
|
-
name: params.name,
|
|
90
|
-
ttl: '30m',
|
|
91
|
-
});
|
|
92
|
-
token.addGrant({
|
|
93
|
-
room: params.roomName,
|
|
94
|
-
roomJoin: true,
|
|
95
|
-
roomCreate: true,
|
|
96
|
-
canPublish: true,
|
|
97
|
-
canSubscribe: true,
|
|
98
|
-
});
|
|
99
|
-
return await token.toJwt();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function connectTestRooms(count: number): Promise<{ roomName: string; rooms: Room[] }> {
|
|
103
|
-
const env = getTestEnv();
|
|
104
|
-
const roomName = `test_room_${randomUUID()}`;
|
|
105
|
-
const rooms = await Promise.all(
|
|
106
|
-
Array.from({ length: count }, async (_, i) => {
|
|
107
|
-
const token = await createJoinToken({
|
|
108
|
-
env,
|
|
109
|
-
roomName,
|
|
110
|
-
identity: `p${i}`,
|
|
111
|
-
name: `Participant ${i}`,
|
|
112
|
-
});
|
|
113
|
-
const room = new Room();
|
|
114
|
-
await room.connect(env.url, token, { autoSubscribe: true, dynacast: false });
|
|
115
|
-
return room;
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const start = Date.now();
|
|
120
|
-
await waitFor(() => rooms.every((r) => r.remoteParticipants.size === count - 1), {
|
|
121
|
-
timeoutMs: 5000,
|
|
122
|
-
debugName: `participant visibility (${Date.now() - start}ms)`,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return { roomName, rooms };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function waitForRoomEvent<R>(
|
|
129
|
-
room: Room,
|
|
130
|
-
event: RoomEvent,
|
|
131
|
-
timeoutMs: number,
|
|
132
|
-
take: (...args: any[]) => R,
|
|
133
|
-
match?: (...args: unknown[]) => boolean,
|
|
134
|
-
): Promise<R> {
|
|
135
|
-
return withTimeout(
|
|
136
|
-
new Promise<R>((resolve) => {
|
|
137
|
-
const handler = (...args: any[]) => {
|
|
138
|
-
if (match && !match(...args)) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
// typed-emitter doesn't expose `.once` in the type surface, so do manual once+cleanup.
|
|
142
|
-
room.off(event as any, handler as any);
|
|
143
|
-
resolve(take(...args));
|
|
144
|
-
};
|
|
145
|
-
room.on(event as any, handler as any);
|
|
146
|
-
}),
|
|
147
|
-
timeoutMs,
|
|
148
|
-
`Timed out waiting for ${event}`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
34
|
/**
|
|
153
35
|
* Only tracks published by `identity` are the test's business. Anything else in
|
|
154
36
|
* the room — an agent the project dispatches, a stray participant — publishes
|
|
@@ -182,17 +64,6 @@ function expectTimestampFromCall(timestamp: number, calledAt: number, what: stri
|
|
|
182
64
|
expect(timestamp, detail).toBeLessThanOrEqual(now + clockToleranceMs);
|
|
183
65
|
}
|
|
184
66
|
|
|
185
|
-
function concatUint8(chunks: Uint8Array[]): Uint8Array {
|
|
186
|
-
const len = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
187
|
-
const out = new Uint8Array(len);
|
|
188
|
-
let offset = 0;
|
|
189
|
-
for (const c of chunks) {
|
|
190
|
-
out.set(c, offset);
|
|
191
|
-
offset += c.byteLength;
|
|
192
|
-
}
|
|
193
|
-
return out;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
67
|
function channelSamples(frame: AudioFrame, channelIndex: number): Int16Array {
|
|
197
68
|
const { data, channels, samplesPerChannel } = frame;
|
|
198
69
|
const out = new Int16Array(samplesPerChannel);
|
|
@@ -296,6 +167,46 @@ describeE2E('livekit-rtc e2e', () => {
|
|
|
296
167
|
testTimeoutMs,
|
|
297
168
|
);
|
|
298
169
|
|
|
170
|
+
it(
|
|
171
|
+
'sends and receives a data message between participants sharing a key',
|
|
172
|
+
async () => {
|
|
173
|
+
// A shared-key `keyProviderOptions` with only `sharedKey` set relies on
|
|
174
|
+
// the SDK filling in the remaining (proto-required) provider defaults;
|
|
175
|
+
// regression guard for connect failing to encode KeyProviderOptions.
|
|
176
|
+
const encryption = {
|
|
177
|
+
keyProviderOptions: { sharedKey: new TextEncoder().encode('test-shared-key') },
|
|
178
|
+
};
|
|
179
|
+
const { rooms } = await connectTestRooms(2, { encryption });
|
|
180
|
+
const [receivingRoom, sendingRoom] = rooms;
|
|
181
|
+
const senderIdentity = sendingRoom!.localParticipant!.identity;
|
|
182
|
+
const receiverIdentity = receivingRoom!.localParticipant!.identity;
|
|
183
|
+
|
|
184
|
+
const payload = new TextEncoder().encode('e2ee-hello');
|
|
185
|
+
const received = withTimeout(
|
|
186
|
+
new Promise<Uint8Array>((resolve) => {
|
|
187
|
+
receivingRoom!.on(RoomEvent.DataReceived, (data, participant) => {
|
|
188
|
+
if (participant?.identity !== senderIdentity) return;
|
|
189
|
+
resolve(data);
|
|
190
|
+
});
|
|
191
|
+
}),
|
|
192
|
+
testTimeoutMs,
|
|
193
|
+
'Timed out waiting for encrypted data message',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await sendingRoom!.localParticipant!.publishData(payload, {
|
|
197
|
+
reliable: true,
|
|
198
|
+
destination_identities: [receiverIdentity],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// The message round-trips intact only if both sides brought a working
|
|
202
|
+
// shared-key provider through connect and encrypt/decrypt lined up.
|
|
203
|
+
expect(await received).toEqual(payload);
|
|
204
|
+
|
|
205
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
206
|
+
},
|
|
207
|
+
testTimeoutMs,
|
|
208
|
+
);
|
|
209
|
+
|
|
299
210
|
it(
|
|
300
211
|
'emits participantDisconnected when a participant leaves',
|
|
301
212
|
async () => {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { describe } from 'vitest';
|
|
8
|
+
import { Room, type RoomEvent, type RoomOptions } from '../index.js';
|
|
9
|
+
|
|
10
|
+
export const hasE2EEnv =
|
|
11
|
+
!!process.env.LIVEKIT_URL && !!process.env.LIVEKIT_API_KEY && !!process.env.LIVEKIT_API_SECRET;
|
|
12
|
+
// Explicitly typed so declaration emit doesn't reference vitest's
|
|
13
|
+
// non-exported internal suite types (TS4023).
|
|
14
|
+
export const describeE2E: typeof describe = hasE2EEnv
|
|
15
|
+
? describe
|
|
16
|
+
: (describe.skip as typeof describe);
|
|
17
|
+
export const testTimeoutMs = 10_000;
|
|
18
|
+
|
|
19
|
+
export type TestEnv = {
|
|
20
|
+
url: string;
|
|
21
|
+
apiKey: string;
|
|
22
|
+
apiSecret: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function normalizeLiveKitUrl(url: string): string {
|
|
26
|
+
if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
|
|
27
|
+
if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getTestEnv(): TestEnv {
|
|
32
|
+
if (!hasE2EEnv) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Missing required env vars for e2e: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
url: normalizeLiveKitUrl(process.env.LIVEKIT_URL!),
|
|
39
|
+
apiKey: process.env.LIVEKIT_API_KEY!,
|
|
40
|
+
apiSecret: process.env.LIVEKIT_API_SECRET!,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function withTimeout<T>(
|
|
45
|
+
promise: Promise<T>,
|
|
46
|
+
timeoutMs: number,
|
|
47
|
+
message: string,
|
|
48
|
+
): Promise<T> {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
(async () => {
|
|
52
|
+
await delay(timeoutMs);
|
|
53
|
+
throw new Error(message);
|
|
54
|
+
})(),
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function waitFor(
|
|
59
|
+
condition: () => boolean,
|
|
60
|
+
opts: { timeoutMs: number; intervalMs?: number; debugName?: string },
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const intervalMs = opts.intervalMs ?? 50;
|
|
63
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
if (condition()) return;
|
|
66
|
+
await delay(intervalMs);
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Timed out waiting for condition${opts.debugName ? ` (${opts.debugName})` : ''}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createJoinToken(params: {
|
|
72
|
+
env: TestEnv;
|
|
73
|
+
roomName: string;
|
|
74
|
+
identity: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}): Promise<string> {
|
|
77
|
+
const token = new AccessToken(params.env.apiKey, params.env.apiSecret, {
|
|
78
|
+
identity: params.identity,
|
|
79
|
+
name: params.name,
|
|
80
|
+
ttl: '30m',
|
|
81
|
+
});
|
|
82
|
+
token.addGrant({
|
|
83
|
+
room: params.roomName,
|
|
84
|
+
roomJoin: true,
|
|
85
|
+
roomCreate: true,
|
|
86
|
+
canPublish: true,
|
|
87
|
+
canSubscribe: true,
|
|
88
|
+
});
|
|
89
|
+
return await token.toJwt();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function connectTestRooms(
|
|
93
|
+
count: number,
|
|
94
|
+
/**
|
|
95
|
+
* Extra room options applied to every room, e.g. data stream limits or
|
|
96
|
+
* `encryption` to bring up the whole set with a shared e2ee key.
|
|
97
|
+
*/
|
|
98
|
+
options?: Partial<RoomOptions>,
|
|
99
|
+
): Promise<{ roomName: string; rooms: Room[] }> {
|
|
100
|
+
const env = getTestEnv();
|
|
101
|
+
const roomName = `test_room_${randomUUID()}`;
|
|
102
|
+
const rooms = await Promise.all(
|
|
103
|
+
Array.from({ length: count }, async (_, i) => {
|
|
104
|
+
const token = await createJoinToken({
|
|
105
|
+
env,
|
|
106
|
+
roomName,
|
|
107
|
+
identity: `p${i}`,
|
|
108
|
+
name: `Participant ${i}`,
|
|
109
|
+
});
|
|
110
|
+
const room = new Room();
|
|
111
|
+
await room.connect(env.url, token, {
|
|
112
|
+
autoSubscribe: true,
|
|
113
|
+
dynacast: false,
|
|
114
|
+
...options,
|
|
115
|
+
});
|
|
116
|
+
return room;
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
await waitFor(() => rooms.every((r) => r.remoteParticipants.size === count - 1), {
|
|
122
|
+
timeoutMs: 5000,
|
|
123
|
+
debugName: `participant visibility (${Date.now() - start}ms)`,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { roomName, rooms };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function waitForRoomEvent<R>(
|
|
130
|
+
room: Room,
|
|
131
|
+
event: RoomEvent,
|
|
132
|
+
timeoutMs: number,
|
|
133
|
+
take: (...args: any[]) => R,
|
|
134
|
+
/** Ignore emissions that don't match, e.g. events for another participant. */
|
|
135
|
+
match?: (...args: unknown[]) => boolean,
|
|
136
|
+
): Promise<R> {
|
|
137
|
+
return withTimeout(
|
|
138
|
+
new Promise<R>((resolve) => {
|
|
139
|
+
const handler = (...args: any[]) => {
|
|
140
|
+
if (match && !match(...args)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// typed-emitter doesn't expose `.once` in the type surface, so do manual once+cleanup.
|
|
144
|
+
room.off(event as any, handler as any);
|
|
145
|
+
resolve(take(...args));
|
|
146
|
+
};
|
|
147
|
+
room.on(event as any, handler as any);
|
|
148
|
+
}),
|
|
149
|
+
timeoutMs,
|
|
150
|
+
`Timed out waiting for ${event}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function concatUint8(chunks: Uint8Array[]): Uint8Array {
|
|
155
|
+
const len = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
156
|
+
const out = new Uint8Array(len);
|
|
157
|
+
let offset = 0;
|
|
158
|
+
for (const c of chunks) {
|
|
159
|
+
out.set(c, offset);
|
|
160
|
+
offset += c.byteLength;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|