@livekit/rtc-node 0.13.23 → 0.13.25
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_filter.cjs +2 -2
- package/dist/audio_filter.cjs.map +1 -1
- package/dist/audio_filter.js +1 -1
- package/dist/audio_filter.js.map +1 -1
- package/dist/audio_frame.cjs +11 -7
- package/dist/audio_frame.cjs.map +1 -1
- package/dist/audio_frame.d.cts +3 -8
- package/dist/audio_frame.d.ts +3 -8
- package/dist/audio_frame.d.ts.map +1 -1
- package/dist/audio_frame.js +10 -6
- package/dist/audio_frame.js.map +1 -1
- package/dist/audio_mixer.cjs +11 -4
- package/dist/audio_mixer.cjs.map +1 -1
- package/dist/audio_mixer.d.cts +1 -6
- package/dist/audio_mixer.d.ts +1 -6
- package/dist/audio_mixer.d.ts.map +1 -1
- package/dist/audio_mixer.js +11 -4
- package/dist/audio_mixer.js.map +1 -1
- package/dist/audio_resampler.cjs +20 -12
- package/dist/audio_resampler.cjs.map +1 -1
- package/dist/audio_resampler.d.cts +6 -6
- package/dist/audio_resampler.d.ts +6 -6
- package/dist/audio_resampler.d.ts.map +1 -1
- package/dist/audio_resampler.js +12 -4
- package/dist/audio_resampler.js.map +1 -1
- package/dist/audio_source.cjs +11 -7
- package/dist/audio_source.cjs.map +1 -1
- package/dist/audio_source.d.cts +1 -7
- package/dist/audio_source.d.ts +1 -7
- package/dist/audio_source.d.ts.map +1 -1
- package/dist/audio_source.js +8 -3
- package/dist/audio_source.js.map +1 -1
- package/dist/audio_stream.cjs +17 -5
- package/dist/audio_stream.cjs.map +1 -1
- package/dist/audio_stream.d.cts +1 -14
- package/dist/audio_stream.d.ts +1 -14
- package/dist/audio_stream.d.ts.map +1 -1
- package/dist/audio_stream.js +16 -4
- package/dist/audio_stream.js.map +1 -1
- package/dist/data_streams/index.d.cts +2 -10
- package/dist/data_streams/index.d.ts +2 -10
- package/dist/data_streams/stream_reader.cjs +8 -0
- package/dist/data_streams/stream_reader.cjs.map +1 -1
- package/dist/data_streams/stream_reader.d.cts +2 -10
- package/dist/data_streams/stream_reader.d.ts +2 -10
- package/dist/data_streams/stream_reader.d.ts.map +1 -1
- package/dist/data_streams/stream_reader.js +8 -0
- package/dist/data_streams/stream_reader.js.map +1 -1
- package/dist/data_streams/stream_writer.d.cts +2 -10
- package/dist/data_streams/stream_writer.d.ts +2 -10
- package/dist/data_streams/types.cjs.map +1 -1
- package/dist/data_streams/types.d.cts +2 -10
- package/dist/data_streams/types.d.ts +2 -10
- package/dist/data_streams/types.d.ts.map +1 -1
- package/dist/e2ee.cjs +21 -21
- package/dist/e2ee.cjs.map +1 -1
- package/dist/e2ee.d.cts +1 -2
- package/dist/e2ee.d.ts +1 -2
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +2 -2
- package/dist/e2ee.js.map +1 -1
- package/dist/ffi_client.cjs +11 -12
- package/dist/ffi_client.cjs.map +1 -1
- package/dist/ffi_client.d.cts +3 -16
- package/dist/ffi_client.d.ts +3 -16
- package/dist/ffi_client.d.ts.map +1 -1
- package/dist/ffi_client.js +5 -3
- package/dist/ffi_client.js.map +1 -1
- package/dist/frame_processor.cjs +13 -2
- package/dist/frame_processor.cjs.map +1 -1
- package/dist/frame_processor.d.cts +5 -8
- package/dist/frame_processor.d.ts +5 -8
- package/dist/frame_processor.d.ts.map +1 -1
- package/dist/frame_processor.js +10 -1
- package/dist/frame_processor.js.map +1 -1
- package/dist/index.cjs +25 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -16
- package/dist/index.d.ts +3 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/log.cjs +1 -10
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +4 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -10
- package/dist/log.js.map +1 -1
- package/dist/participant.cjs +38 -38
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -15
- package/dist/participant.d.ts +2 -15
- package/dist/participant.d.ts.map +1 -1
- package/dist/participant.js +12 -12
- package/dist/participant.js.map +1 -1
- package/dist/room.cjs +48 -30
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +11 -16
- package/dist/room.d.ts +11 -16
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +44 -26
- package/dist/room.js.map +1 -1
- package/dist/rpc.cjs.map +1 -1
- package/dist/rpc.d.cts +1 -2
- package/dist/rpc.d.ts +1 -2
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js.map +1 -1
- package/dist/{stream_reader-Ch2zvjU3.d.cts → stream_reader-CuG4b8Y-.d.cts} +1 -1
- package/dist/{stream_reader-DRyR29vo.d.ts → stream_reader-CuG4b8Y-.d.ts} +1 -1
- package/dist/track.cjs +3 -3
- package/dist/track.cjs.map +1 -1
- package/dist/track.d.cts +1 -14
- package/dist/track.d.ts +1 -14
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +1 -1
- package/dist/track.js.map +1 -1
- package/dist/track_publication.cjs +4 -4
- package/dist/track_publication.cjs.map +1 -1
- package/dist/track_publication.d.cts +1 -14
- package/dist/track_publication.d.ts +1 -14
- package/dist/track_publication.d.ts.map +1 -1
- package/dist/track_publication.js +2 -2
- package/dist/track_publication.js.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/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/video_frame.cjs +42 -42
- package/dist/video_frame.cjs.map +1 -1
- package/dist/video_frame.d.cts +1 -6
- package/dist/video_frame.d.ts +1 -6
- package/dist/video_frame.d.ts.map +1 -1
- package/dist/video_frame.js +2 -2
- package/dist/video_frame.js.map +1 -1
- package/dist/video_source.cjs +5 -5
- package/dist/video_source.cjs.map +1 -1
- package/dist/video_source.d.cts +1 -14
- package/dist/video_source.d.ts +1 -14
- package/dist/video_source.d.ts.map +1 -1
- package/dist/video_source.js +2 -2
- package/dist/video_source.js.map +1 -1
- package/dist/video_stream.cjs +12 -4
- package/dist/video_stream.cjs.map +1 -1
- package/dist/video_stream.d.cts +1 -14
- package/dist/video_stream.d.ts +1 -14
- package/dist/video_stream.d.ts.map +1 -1
- package/dist/video_stream.js +10 -2
- package/dist/video_stream.js.map +1 -1
- package/package.json +9 -35
- package/src/audio_filter.ts +2 -2
- package/src/audio_frame.ts +26 -7
- package/src/audio_mixer.ts +15 -4
- package/src/audio_resampler.ts +14 -4
- package/src/audio_source.ts +13 -5
- package/src/audio_stream.ts +21 -5
- package/src/data_streams/stream_reader.ts +19 -1
- package/src/data_streams/types.ts +1 -1
- package/src/e2ee.ts +3 -3
- package/src/ffi_client.ts +7 -5
- package/src/frame_processor.ts +21 -0
- package/src/index.ts +6 -5
- package/src/log.ts +14 -10
- package/src/participant.ts +17 -17
- package/src/room.ts +65 -38
- package/src/rpc.ts +1 -1
- package/src/tests/e2e.test.ts +517 -0
- package/src/track.ts +4 -4
- package/src/track_publication.ts +6 -6
- package/src/version.ts +1 -1
- package/src/video_frame.ts +3 -3
- package/src/video_source.ts +3 -3
- package/src/video_stream.ts +15 -3
- package/Cargo.toml +0 -25
- package/build.rs +0 -9
- package/dist/napi/native.cjs +0 -301
- package/dist/napi/native.d.cjs +0 -17
- package/dist/napi/native.d.cjs.map +0 -1
- package/dist/napi/native.d.d.cts +0 -21
- package/dist/napi/native.d.d.ts +0 -21
- package/dist/napi/native.d.js +0 -1
- package/dist/napi/native.d.js.map +0 -1
- package/dist/napi/native.d.ts +0 -19
- package/dist/napi/native.js +0 -15
- package/dist/native.cjs +0 -301
- package/dist/native.d.ts +0 -19
- package/dist/native.js +0 -15
- package/dist/proto/audio_frame_pb.cjs +0 -1357
- package/dist/proto/audio_frame_pb.cjs.map +0 -1
- package/dist/proto/audio_frame_pb.d.cts +0 -1183
- package/dist/proto/audio_frame_pb.d.ts +0 -1183
- package/dist/proto/audio_frame_pb.d.ts.map +0 -1
- package/dist/proto/audio_frame_pb.js +0 -1283
- package/dist/proto/audio_frame_pb.js.map +0 -1
- package/dist/proto/data_stream_pb.cjs +0 -1638
- package/dist/proto/data_stream_pb.cjs.map +0 -1
- package/dist/proto/data_stream_pb.d.cts +0 -1449
- package/dist/proto/data_stream_pb.d.ts +0 -1449
- package/dist/proto/data_stream_pb.d.ts.map +0 -1
- package/dist/proto/data_stream_pb.js +0 -1559
- package/dist/proto/data_stream_pb.js.map +0 -1
- package/dist/proto/e2ee_pb.cjs +0 -746
- package/dist/proto/e2ee_pb.cjs.map +0 -1
- package/dist/proto/e2ee_pb.d.cts +0 -653
- package/dist/proto/e2ee_pb.d.ts +0 -653
- package/dist/proto/e2ee_pb.d.ts.map +0 -1
- package/dist/proto/e2ee_pb.js +0 -696
- package/dist/proto/e2ee_pb.js.map +0 -1
- package/dist/proto/ffi_pb.cjs +0 -477
- package/dist/proto/ffi_pb.cjs.map +0 -1
- package/dist/proto/ffi_pb.d.cts +0 -1321
- package/dist/proto/ffi_pb.d.ts +0 -1321
- package/dist/proto/ffi_pb.d.ts.map +0 -1
- package/dist/proto/ffi_pb.js +0 -444
- package/dist/proto/ffi_pb.js.map +0 -1
- package/dist/proto/handle_pb.cjs +0 -53
- package/dist/proto/handle_pb.cjs.map +0 -1
- package/dist/proto/handle_pb.d.cts +0 -31
- package/dist/proto/handle_pb.d.ts +0 -31
- package/dist/proto/handle_pb.d.ts.map +0 -1
- package/dist/proto/handle_pb.js +0 -29
- package/dist/proto/handle_pb.js.map +0 -1
- package/dist/proto/participant_pb.cjs +0 -173
- package/dist/proto/participant_pb.cjs.map +0 -1
- package/dist/proto/participant_pb.d.cts +0 -218
- package/dist/proto/participant_pb.d.ts +0 -218
- package/dist/proto/participant_pb.d.ts.map +0 -1
- package/dist/proto/participant_pb.js +0 -145
- package/dist/proto/participant_pb.js.map +0 -1
- package/dist/proto/room_pb.cjs +0 -3374
- package/dist/proto/room_pb.cjs.map +0 -1
- package/dist/proto/room_pb.d.cts +0 -3028
- package/dist/proto/room_pb.d.ts +0 -3028
- package/dist/proto/room_pb.d.ts.map +0 -1
- package/dist/proto/room_pb.js +0 -3232
- package/dist/proto/room_pb.js.map +0 -1
- package/dist/proto/rpc_pb.cjs +0 -328
- package/dist/proto/rpc_pb.cjs.map +0 -1
- package/dist/proto/rpc_pb.d.cts +0 -267
- package/dist/proto/rpc_pb.d.ts +0 -267
- package/dist/proto/rpc_pb.d.ts.map +0 -1
- package/dist/proto/rpc_pb.js +0 -294
- package/dist/proto/rpc_pb.js.map +0 -1
- package/dist/proto/stats_pb.cjs +0 -1370
- package/dist/proto/stats_pb.cjs.map +0 -1
- package/dist/proto/stats_pb.d.cts +0 -1764
- package/dist/proto/stats_pb.d.ts +0 -1764
- package/dist/proto/stats_pb.d.ts.map +0 -1
- package/dist/proto/stats_pb.js +0 -1300
- package/dist/proto/stats_pb.js.map +0 -1
- package/dist/proto/track_pb.cjs +0 -630
- package/dist/proto/track_pb.cjs.map +0 -1
- package/dist/proto/track_pb.d.cts +0 -542
- package/dist/proto/track_pb.d.ts +0 -542
- package/dist/proto/track_pb.d.ts.map +0 -1
- package/dist/proto/track_pb.js +0 -584
- package/dist/proto/track_pb.js.map +0 -1
- package/dist/proto/track_publication_pb.cjs +0 -194
- package/dist/proto/track_publication_pb.cjs.map +0 -1
- package/dist/proto/track_publication_pb.d.cts +0 -135
- package/dist/proto/track_publication_pb.d.ts +0 -135
- package/dist/proto/track_publication_pb.d.ts.map +0 -1
- package/dist/proto/track_publication_pb.js +0 -164
- package/dist/proto/track_publication_pb.js.map +0 -1
- package/dist/proto/video_frame_pb.cjs +0 -723
- package/dist/proto/video_frame_pb.cjs.map +0 -1
- package/dist/proto/video_frame_pb.d.cts +0 -667
- package/dist/proto/video_frame_pb.d.ts +0 -667
- package/dist/proto/video_frame_pb.d.ts.map +0 -1
- package/dist/proto/video_frame_pb.js +0 -673
- package/dist/proto/video_frame_pb.js.map +0 -1
- package/src/lib.rs +0 -9
- package/src/napi/native.cjs +0 -301
- package/src/napi/native.d.ts +0 -19
- package/src/napi/native.js +0 -15
- package/src/nodejs.rs +0 -134
- package/src/proto/audio_frame_pb.ts +0 -2270
- package/src/proto/data_stream_pb.ts +0 -2674
- package/src/proto/e2ee_pb.ts +0 -1231
- package/src/proto/ffi_pb.ts +0 -1700
- package/src/proto/handle_pb.ts +0 -69
- package/src/proto/participant_pb.ts +0 -351
- package/src/proto/room_pb.ts +0 -5661
- package/src/proto/rpc_pb.ts +0 -539
- package/src/proto/stats_pb.ts +0 -3050
- package/src/proto/track_pb.ts +0 -1040
- package/src/proto/track_publication_pb.ts +0 -284
- package/src/proto/video_frame_pb.ts +0 -1236
|
@@ -0,0 +1,517 @@
|
|
|
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 { afterAll, describe, expect, it as itRaw } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
AudioFrame,
|
|
10
|
+
AudioSource,
|
|
11
|
+
AudioStream,
|
|
12
|
+
ConnectionState,
|
|
13
|
+
LocalAudioTrack,
|
|
14
|
+
ParticipantKind,
|
|
15
|
+
Room,
|
|
16
|
+
RoomEvent,
|
|
17
|
+
RpcError,
|
|
18
|
+
TrackPublishOptions,
|
|
19
|
+
TrackSource,
|
|
20
|
+
dispose,
|
|
21
|
+
} from '../index.js';
|
|
22
|
+
|
|
23
|
+
// use concurrent testing if available on the runner (currently not supported by bun's api)
|
|
24
|
+
const it = typeof itRaw.concurrent === 'function' ? itRaw.concurrent : itRaw;
|
|
25
|
+
|
|
26
|
+
const hasE2EEnv =
|
|
27
|
+
!!process.env.LIVEKIT_URL && !!process.env.LIVEKIT_API_KEY && !!process.env.LIVEKIT_API_SECRET;
|
|
28
|
+
const describeE2E = hasE2EEnv ? describe : describe.skip;
|
|
29
|
+
const testTimeoutMs = 10_000;
|
|
30
|
+
|
|
31
|
+
type TestEnv = {
|
|
32
|
+
url: string;
|
|
33
|
+
apiKey: string;
|
|
34
|
+
apiSecret: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function normalizeLiveKitUrl(url: string): string {
|
|
38
|
+
if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
|
|
39
|
+
if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTestEnv(): TestEnv {
|
|
44
|
+
if (!hasE2EEnv) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Missing required env vars for e2e: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
url: normalizeLiveKitUrl(process.env.LIVEKIT_URL!),
|
|
51
|
+
apiKey: process.env.LIVEKIT_API_KEY!,
|
|
52
|
+
apiSecret: process.env.LIVEKIT_API_SECRET!,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
57
|
+
return await Promise.race([
|
|
58
|
+
promise,
|
|
59
|
+
(async () => {
|
|
60
|
+
await delay(timeoutMs);
|
|
61
|
+
throw new Error(message);
|
|
62
|
+
})(),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function waitFor(
|
|
67
|
+
condition: () => boolean,
|
|
68
|
+
opts: { timeoutMs: number; intervalMs?: number; debugName?: string },
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const intervalMs = opts.intervalMs ?? 50;
|
|
71
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
if (condition()) return;
|
|
74
|
+
await delay(intervalMs);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Timed out waiting for condition${opts.debugName ? ` (${opts.debugName})` : ''}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function createJoinToken(params: {
|
|
80
|
+
env: TestEnv;
|
|
81
|
+
roomName: string;
|
|
82
|
+
identity: string;
|
|
83
|
+
name: string;
|
|
84
|
+
}): Promise<string> {
|
|
85
|
+
const token = new AccessToken(params.env.apiKey, params.env.apiSecret, {
|
|
86
|
+
identity: params.identity,
|
|
87
|
+
name: params.name,
|
|
88
|
+
ttl: '30m',
|
|
89
|
+
});
|
|
90
|
+
token.addGrant({
|
|
91
|
+
room: params.roomName,
|
|
92
|
+
roomJoin: true,
|
|
93
|
+
roomCreate: true,
|
|
94
|
+
canPublish: true,
|
|
95
|
+
canSubscribe: true,
|
|
96
|
+
});
|
|
97
|
+
return await token.toJwt();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function connectTestRooms(count: number): Promise<{ roomName: string; rooms: Room[] }> {
|
|
101
|
+
const env = getTestEnv();
|
|
102
|
+
const roomName = `test_room_${randomUUID()}`;
|
|
103
|
+
const rooms = await Promise.all(
|
|
104
|
+
Array.from({ length: count }, async (_, i) => {
|
|
105
|
+
const token = await createJoinToken({
|
|
106
|
+
env,
|
|
107
|
+
roomName,
|
|
108
|
+
identity: `p${i}`,
|
|
109
|
+
name: `Participant ${i}`,
|
|
110
|
+
});
|
|
111
|
+
const room = new Room();
|
|
112
|
+
await room.connect(env.url, token, { autoSubscribe: true, dynacast: false });
|
|
113
|
+
return room;
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
await waitFor(() => rooms.every((r) => r.remoteParticipants.size === count - 1), {
|
|
119
|
+
timeoutMs: 5000,
|
|
120
|
+
debugName: `participant visibility (${Date.now() - start}ms)`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { roomName, rooms };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function waitForRoomEvent<R>(
|
|
127
|
+
room: Room,
|
|
128
|
+
event: RoomEvent,
|
|
129
|
+
timeoutMs: number,
|
|
130
|
+
take: (...args: any[]) => R,
|
|
131
|
+
): Promise<R> {
|
|
132
|
+
return withTimeout(
|
|
133
|
+
new Promise<R>((resolve) => {
|
|
134
|
+
const handler = (...args: any[]) => {
|
|
135
|
+
// typed-emitter doesn't expose `.once` in the type surface, so do manual once+cleanup.
|
|
136
|
+
room.off(event as any, handler as any);
|
|
137
|
+
resolve(take(...args));
|
|
138
|
+
};
|
|
139
|
+
room.on(event as any, handler as any);
|
|
140
|
+
}),
|
|
141
|
+
timeoutMs,
|
|
142
|
+
`Timed out waiting for ${event}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function concatUint8(chunks: Uint8Array[]): Uint8Array {
|
|
147
|
+
const len = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
148
|
+
const out = new Uint8Array(len);
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (const c of chunks) {
|
|
151
|
+
out.set(c, offset);
|
|
152
|
+
offset += c.byteLength;
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function channelSamples(frame: AudioFrame, channelIndex: number): Int16Array {
|
|
158
|
+
const { data, channels, samplesPerChannel } = frame;
|
|
159
|
+
const out = new Int16Array(samplesPerChannel);
|
|
160
|
+
for (let i = 0; i < samplesPerChannel; i++) {
|
|
161
|
+
out[i] = data[i * channels + channelIndex]!;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function estimateFreqHz(samples: Int16Array, sampleRate: number): number {
|
|
167
|
+
if (samples.length < sampleRate / 10) {
|
|
168
|
+
// need at least ~100ms for low-frequency signals
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// basic autocorrelation over a plausible band around 60Hz
|
|
173
|
+
const expectedHz = 60;
|
|
174
|
+
const minHz = 20;
|
|
175
|
+
const maxHz = 200;
|
|
176
|
+
const minLag = Math.floor(sampleRate / maxHz);
|
|
177
|
+
const maxLag = Math.floor(sampleRate / minHz);
|
|
178
|
+
const expectedLag = Math.floor(sampleRate / expectedHz);
|
|
179
|
+
const searchRadius = Math.floor(expectedLag * 0.5);
|
|
180
|
+
const lagStart = Math.max(minLag, expectedLag - searchRadius);
|
|
181
|
+
const lagEnd = Math.min(maxLag, expectedLag + searchRadius);
|
|
182
|
+
|
|
183
|
+
const x = new Float64Array(samples.length);
|
|
184
|
+
let mean = 0;
|
|
185
|
+
for (let i = 0; i < samples.length; i++) mean += samples[i]!;
|
|
186
|
+
mean /= samples.length;
|
|
187
|
+
for (let i = 0; i < samples.length; i++) x[i] = samples[i]! - mean;
|
|
188
|
+
|
|
189
|
+
let bestLag = 0;
|
|
190
|
+
let bestCorr = -Infinity;
|
|
191
|
+
for (let lag = lagStart; lag <= lagEnd; lag++) {
|
|
192
|
+
let corr = 0;
|
|
193
|
+
for (let i = 0; i < x.length - lag; i++) corr += x[i]! * x[i + lag]!;
|
|
194
|
+
if (corr > bestCorr) {
|
|
195
|
+
bestCorr = corr;
|
|
196
|
+
bestLag = lag;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return bestLag > 0 ? sampleRate / bestLag : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
describeE2E('livekit-rtc e2e', () => {
|
|
204
|
+
afterAll(async () => {
|
|
205
|
+
await dispose();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it(
|
|
209
|
+
'connects to a room',
|
|
210
|
+
async () => {
|
|
211
|
+
const { roomName, rooms } = await connectTestRooms(1);
|
|
212
|
+
const room = rooms[0]!;
|
|
213
|
+
|
|
214
|
+
expect(room.connectionState).toBe(ConnectionState.CONN_CONNECTED);
|
|
215
|
+
expect(room.name).toBe(roomName);
|
|
216
|
+
expect(room.remoteParticipants.size).toBe(0);
|
|
217
|
+
|
|
218
|
+
expect(room.creationTime.getTime()).toBeGreaterThan(0);
|
|
219
|
+
expect(Math.abs(room.creationTime.getTime() - Date.now())).toBeLessThanOrEqual(10_000);
|
|
220
|
+
|
|
221
|
+
expect(room.localParticipant?.sid).toMatch(/^PA_/);
|
|
222
|
+
expect(room.localParticipant?.identity).toBe('p0');
|
|
223
|
+
expect(room.localParticipant?.name).toBe('Participant 0');
|
|
224
|
+
expect(room.localParticipant?.kind).toBe(ParticipantKind.STANDARD);
|
|
225
|
+
|
|
226
|
+
await room.disconnect();
|
|
227
|
+
},
|
|
228
|
+
testTimeoutMs,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
it(
|
|
232
|
+
'connects multiple participants to the same room',
|
|
233
|
+
async () => {
|
|
234
|
+
const { roomName, rooms } = await connectTestRooms(2);
|
|
235
|
+
const [first, second] = rooms;
|
|
236
|
+
|
|
237
|
+
expect(first?.name).toBe(roomName);
|
|
238
|
+
expect(second?.name).toBe(roomName);
|
|
239
|
+
expect(first?.remoteParticipants.get(second!.localParticipant!.identity)).toBeTruthy();
|
|
240
|
+
expect(second?.remoteParticipants.get(first!.localParticipant!.identity)).toBeTruthy();
|
|
241
|
+
|
|
242
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
243
|
+
},
|
|
244
|
+
testTimeoutMs,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
it(
|
|
248
|
+
'emits participantDisconnected when a participant leaves',
|
|
249
|
+
async () => {
|
|
250
|
+
const { rooms } = await connectTestRooms(2);
|
|
251
|
+
const [first, second] = rooms;
|
|
252
|
+
const secondIdentity = second!.localParticipant!.identity;
|
|
253
|
+
const secondName = second!.localParticipant!.name!;
|
|
254
|
+
|
|
255
|
+
const disconnected = waitForRoomEvent(
|
|
256
|
+
first!,
|
|
257
|
+
RoomEvent.ParticipantDisconnected,
|
|
258
|
+
testTimeoutMs,
|
|
259
|
+
(p: { identity: string; name?: string }) => ({ identity: p.identity, name: p.name ?? '' }),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await second!.disconnect();
|
|
263
|
+
|
|
264
|
+
const ev = await disconnected;
|
|
265
|
+
expect(ev.identity).toBe(secondIdentity);
|
|
266
|
+
expect(ev.name).toBe(secondName);
|
|
267
|
+
|
|
268
|
+
await first!.disconnect();
|
|
269
|
+
},
|
|
270
|
+
testTimeoutMs,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
it(
|
|
274
|
+
'transfers audio between two participants (sine detection)',
|
|
275
|
+
async () => {
|
|
276
|
+
const cases = [
|
|
277
|
+
{ pubRateHz: 48_000, pubChannels: 1, subRateHz: 48_000, subChannels: 1 },
|
|
278
|
+
{ pubRateHz: 48_000, pubChannels: 2, subRateHz: 48_000, subChannels: 2 },
|
|
279
|
+
{ pubRateHz: 48_000, pubChannels: 2, subRateHz: 24_000, subChannels: 2 },
|
|
280
|
+
{ pubRateHz: 24_000, pubChannels: 2, subRateHz: 24_000, subChannels: 1 },
|
|
281
|
+
] as const;
|
|
282
|
+
|
|
283
|
+
for (const params of cases) {
|
|
284
|
+
const { rooms } = await connectTestRooms(2);
|
|
285
|
+
const [subRoom, pubRoom] = rooms;
|
|
286
|
+
|
|
287
|
+
const subscribed = waitForRoomEvent(
|
|
288
|
+
subRoom!,
|
|
289
|
+
RoomEvent.TrackSubscribed,
|
|
290
|
+
15_000,
|
|
291
|
+
(track: unknown) => track,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const source = new AudioSource(params.pubRateHz, params.pubChannels);
|
|
295
|
+
const track = LocalAudioTrack.createAudioTrack('sine', source);
|
|
296
|
+
const options = new TrackPublishOptions();
|
|
297
|
+
options.source = TrackSource.SOURCE_MICROPHONE;
|
|
298
|
+
await pubRoom!.localParticipant!.publishTrack(track, options);
|
|
299
|
+
|
|
300
|
+
const remoteTrack = await subscribed;
|
|
301
|
+
const stream = new AudioStream(remoteTrack as any, {
|
|
302
|
+
sampleRate: params.subRateHz,
|
|
303
|
+
numChannels: params.subChannels,
|
|
304
|
+
});
|
|
305
|
+
const reader = stream.getReader();
|
|
306
|
+
|
|
307
|
+
const sineHz = 60;
|
|
308
|
+
const framesToAnalyze = 100;
|
|
309
|
+
const collected: Int16Array[] = Array.from(
|
|
310
|
+
{ length: params.subChannels },
|
|
311
|
+
() => new Int16Array(0),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const readTask = (async () => {
|
|
315
|
+
let frames = 0;
|
|
316
|
+
while (frames < framesToAnalyze) {
|
|
317
|
+
const { done, value } = await reader.read();
|
|
318
|
+
if (done) break;
|
|
319
|
+
expect(value.sampleRate).toBe(params.subRateHz);
|
|
320
|
+
expect(value.channels).toBe(params.subChannels);
|
|
321
|
+
for (let ch = 0; ch < params.subChannels; ch++) {
|
|
322
|
+
const s = channelSamples(value, ch);
|
|
323
|
+
const prev = collected[ch]!;
|
|
324
|
+
const next = new Int16Array(prev.length + s.length);
|
|
325
|
+
next.set(prev, 0);
|
|
326
|
+
next.set(s, prev.length);
|
|
327
|
+
collected[ch] = next;
|
|
328
|
+
}
|
|
329
|
+
frames++;
|
|
330
|
+
}
|
|
331
|
+
expect(frames).toBe(framesToAnalyze);
|
|
332
|
+
})();
|
|
333
|
+
|
|
334
|
+
const samplesPer10ms = Math.floor(params.pubRateHz / 100);
|
|
335
|
+
const amplitude = 0.8 * 32767;
|
|
336
|
+
const publishTask = (async () => {
|
|
337
|
+
let t = 0;
|
|
338
|
+
for (let i = 0; i < framesToAnalyze + 20; i++) {
|
|
339
|
+
const frame = AudioFrame.create(params.pubRateHz, params.pubChannels, samplesPer10ms);
|
|
340
|
+
for (let s = 0; s < samplesPer10ms; s++) {
|
|
341
|
+
const v = Math.round(
|
|
342
|
+
amplitude * Math.sin((2 * Math.PI * sineHz * t) / params.pubRateHz),
|
|
343
|
+
);
|
|
344
|
+
t++;
|
|
345
|
+
for (let ch = 0; ch < params.pubChannels; ch++) {
|
|
346
|
+
frame.data[s * params.pubChannels + ch] = v;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await source.captureFrame(frame);
|
|
350
|
+
}
|
|
351
|
+
await source.waitForPlayout();
|
|
352
|
+
})();
|
|
353
|
+
|
|
354
|
+
await withTimeout(
|
|
355
|
+
Promise.all([readTask, publishTask]),
|
|
356
|
+
20_000,
|
|
357
|
+
'Timed out during audio test',
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
for (let ch = 0; ch < params.subChannels; ch++) {
|
|
361
|
+
const detected = estimateFreqHz(collected[ch]!, params.subRateHz);
|
|
362
|
+
expect(Math.abs(detected - sineHz)).toBeLessThan(20);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
reader.releaseLock();
|
|
366
|
+
await track.close();
|
|
367
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
testTimeoutMs * 2,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
it(
|
|
374
|
+
'publishes and receives reliable data packets',
|
|
375
|
+
async () => {
|
|
376
|
+
const { rooms } = await connectTestRooms(2);
|
|
377
|
+
const [receivingRoom, sendingRoom] = rooms;
|
|
378
|
+
const receiverIdentity = receivingRoom!.localParticipant!.identity;
|
|
379
|
+
|
|
380
|
+
const iterations = 128;
|
|
381
|
+
const payload = new Uint8Array(4096).fill(0xfa);
|
|
382
|
+
let received = 0;
|
|
383
|
+
|
|
384
|
+
const receiveTask = withTimeout(
|
|
385
|
+
new Promise<void>((resolve) => {
|
|
386
|
+
receivingRoom!.on(RoomEvent.DataReceived, (data, participant) => {
|
|
387
|
+
if (participant?.identity !== sendingRoom!.localParticipant!.identity) return;
|
|
388
|
+
if (data.byteLength !== payload.byteLength) return;
|
|
389
|
+
received++;
|
|
390
|
+
if (received === iterations) resolve();
|
|
391
|
+
});
|
|
392
|
+
}),
|
|
393
|
+
testTimeoutMs,
|
|
394
|
+
'Timed out waiting for all reliable packets',
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const sendTask = (async () => {
|
|
398
|
+
for (let i = 0; i < iterations; i++) {
|
|
399
|
+
await sendingRoom!.localParticipant!.publishData(payload, {
|
|
400
|
+
reliable: true,
|
|
401
|
+
destination_identities: [receiverIdentity],
|
|
402
|
+
});
|
|
403
|
+
await delay(10);
|
|
404
|
+
}
|
|
405
|
+
})();
|
|
406
|
+
|
|
407
|
+
await Promise.all([receiveTask, sendTask]);
|
|
408
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
409
|
+
},
|
|
410
|
+
testTimeoutMs,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
it(
|
|
414
|
+
'sends and receives text and byte streams',
|
|
415
|
+
async () => {
|
|
416
|
+
const { rooms } = await connectTestRooms(2);
|
|
417
|
+
const [receivingRoom, sendingRoom] = rooms;
|
|
418
|
+
const senderIdentity = sendingRoom!.localParticipant!.identity;
|
|
419
|
+
|
|
420
|
+
const topic = 'some-topic';
|
|
421
|
+
|
|
422
|
+
const textToSend = 'some-text';
|
|
423
|
+
const receivedText = withTimeout(
|
|
424
|
+
new Promise<string>((resolve) => {
|
|
425
|
+
receivingRoom!.registerTextStreamHandler(topic, async (reader, sender) => {
|
|
426
|
+
expect(sender.identity).toBe(senderIdentity);
|
|
427
|
+
resolve(await reader.readAll());
|
|
428
|
+
});
|
|
429
|
+
}),
|
|
430
|
+
testTimeoutMs,
|
|
431
|
+
'Timed out waiting for text stream',
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const textInfo = await sendingRoom!.localParticipant!.sendText(textToSend, { topic });
|
|
435
|
+
expect(textInfo.streamId).toBeTruthy();
|
|
436
|
+
expect(Math.abs(textInfo.timestamp - Date.now())).toBeLessThanOrEqual(1_000);
|
|
437
|
+
expect(textInfo.mimeType).toBe('text/plain');
|
|
438
|
+
expect(textInfo.topic).toBe(topic);
|
|
439
|
+
|
|
440
|
+
expect(await receivedText).toBe(textToSend);
|
|
441
|
+
|
|
442
|
+
const bytesToSend = new Uint8Array(16).fill(0xfa);
|
|
443
|
+
const receivedBytes = withTimeout(
|
|
444
|
+
new Promise<Uint8Array>((resolve) => {
|
|
445
|
+
receivingRoom!.registerByteStreamHandler(topic, async (reader, sender) => {
|
|
446
|
+
expect(sender.identity).toBe(senderIdentity);
|
|
447
|
+
const chunks = await reader.readAll();
|
|
448
|
+
resolve(concatUint8(chunks));
|
|
449
|
+
});
|
|
450
|
+
}),
|
|
451
|
+
testTimeoutMs,
|
|
452
|
+
'Timed out waiting for byte stream',
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const writer = await sendingRoom!.localParticipant!.streamBytes({
|
|
456
|
+
topic,
|
|
457
|
+
totalSize: bytesToSend.byteLength,
|
|
458
|
+
});
|
|
459
|
+
await writer.write(bytesToSend);
|
|
460
|
+
await writer.close();
|
|
461
|
+
|
|
462
|
+
const byteInfo = writer.info;
|
|
463
|
+
expect(byteInfo.streamId).toBeTruthy();
|
|
464
|
+
expect(Math.abs(byteInfo.timestamp - Date.now())).toBeLessThanOrEqual(1_000);
|
|
465
|
+
expect(byteInfo.mimeType).toBe('application/octet-stream');
|
|
466
|
+
expect(byteInfo.topic).toBe(topic);
|
|
467
|
+
|
|
468
|
+
expect(await receivedBytes).toEqual(bytesToSend);
|
|
469
|
+
|
|
470
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
471
|
+
},
|
|
472
|
+
testTimeoutMs,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
it(
|
|
476
|
+
'invokes RPC methods and returns structured errors',
|
|
477
|
+
async () => {
|
|
478
|
+
const { rooms } = await connectTestRooms(2);
|
|
479
|
+
const [callerRoom, calleeRoom] = rooms;
|
|
480
|
+
|
|
481
|
+
const method = 'test-method';
|
|
482
|
+
const payload = 'test-payload';
|
|
483
|
+
|
|
484
|
+
calleeRoom!.localParticipant!.registerRpcMethod(method, async (data) => data.payload);
|
|
485
|
+
|
|
486
|
+
await expect(
|
|
487
|
+
callerRoom!.localParticipant!.performRpc({
|
|
488
|
+
destinationIdentity: calleeRoom!.localParticipant!.identity,
|
|
489
|
+
method,
|
|
490
|
+
payload,
|
|
491
|
+
responseTimeout: 500,
|
|
492
|
+
}),
|
|
493
|
+
).resolves.toBe(payload);
|
|
494
|
+
|
|
495
|
+
await expect(
|
|
496
|
+
callerRoom!.localParticipant!.performRpc({
|
|
497
|
+
destinationIdentity: calleeRoom!.localParticipant!.identity,
|
|
498
|
+
method: 'unregistered-method',
|
|
499
|
+
payload,
|
|
500
|
+
responseTimeout: 500,
|
|
501
|
+
}),
|
|
502
|
+
).rejects.toMatchObject({ code: RpcError.ErrorCode.UNSUPPORTED_METHOD });
|
|
503
|
+
|
|
504
|
+
await expect(
|
|
505
|
+
callerRoom!.localParticipant!.performRpc({
|
|
506
|
+
destinationIdentity: 'unknown-participant',
|
|
507
|
+
method,
|
|
508
|
+
payload,
|
|
509
|
+
responseTimeout: 500,
|
|
510
|
+
}),
|
|
511
|
+
).rejects.toMatchObject({ code: RpcError.ErrorCode.CONNECTION_TIMEOUT });
|
|
512
|
+
|
|
513
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
514
|
+
},
|
|
515
|
+
testTimeoutMs * 2,
|
|
516
|
+
);
|
|
517
|
+
});
|
package/src/track.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import type { AudioSource } from './audio_source.js';
|
|
5
|
-
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
6
4
|
import type {
|
|
7
5
|
CreateAudioTrackResponse,
|
|
8
6
|
CreateVideoTrackResponse,
|
|
@@ -10,8 +8,10 @@ import type {
|
|
|
10
8
|
StreamState,
|
|
11
9
|
TrackInfo,
|
|
12
10
|
TrackKind,
|
|
13
|
-
} from '
|
|
14
|
-
import { CreateAudioTrackRequest, CreateVideoTrackRequest } from '
|
|
11
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
12
|
+
import { CreateAudioTrackRequest, CreateVideoTrackRequest } from '@livekit/rtc-ffi-bindings';
|
|
13
|
+
import type { AudioSource } from './audio_source.js';
|
|
14
|
+
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
15
15
|
import type { VideoSource } from './video_source.js';
|
|
16
16
|
|
|
17
17
|
export abstract class Track {
|
package/src/track_publication.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import type { SetSubscribedResponse } from './proto/room_pb.js';
|
|
8
|
-
import { SetSubscribedRequest } from './proto/room_pb.js';
|
|
4
|
+
import type { EncryptionType } from '@livekit/rtc-ffi-bindings';
|
|
5
|
+
import type { SetSubscribedResponse } from '@livekit/rtc-ffi-bindings';
|
|
6
|
+
import { SetSubscribedRequest } from '@livekit/rtc-ffi-bindings';
|
|
9
7
|
import type {
|
|
10
8
|
OwnedTrackPublication,
|
|
11
9
|
TrackKind,
|
|
12
10
|
TrackPublicationInfo,
|
|
13
11
|
TrackSource,
|
|
14
|
-
} from '
|
|
12
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
13
|
+
import { FfiHandle } from '@livekit/rtc-ffi-bindings';
|
|
14
|
+
import { FfiClient } from './ffi_client.js';
|
|
15
15
|
import type { Track } from './track.js';
|
|
16
16
|
|
|
17
17
|
export abstract class TrackPublication {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.13.
|
|
1
|
+
export const SDK_VERSION = "0.13.25";
|
package/src/video_frame.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import {
|
|
5
|
-
import type { OwnedVideoBuffer, VideoConvertResponse } from './proto/video_frame_pb.js';
|
|
4
|
+
import type { OwnedVideoBuffer, VideoConvertResponse } from '@livekit/rtc-ffi-bindings';
|
|
6
5
|
import {
|
|
7
6
|
VideoBufferInfo,
|
|
8
7
|
VideoBufferInfo_ComponentInfo,
|
|
9
8
|
VideoBufferType,
|
|
10
|
-
} from '
|
|
9
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
10
|
+
import { FfiClient, FfiHandle, FfiRequest } from './ffi_client.js';
|
|
11
11
|
|
|
12
12
|
export class VideoFrame {
|
|
13
13
|
data: Uint8Array;
|
package/src/video_source.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
5
4
|
import type {
|
|
6
5
|
CaptureVideoFrameResponse,
|
|
7
6
|
NewVideoSourceResponse,
|
|
8
7
|
VideoSourceInfo,
|
|
9
|
-
} from '
|
|
8
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
10
9
|
import {
|
|
11
10
|
CaptureVideoFrameRequest,
|
|
12
11
|
NewVideoSourceRequest,
|
|
13
12
|
VideoRotation,
|
|
14
13
|
VideoSourceType,
|
|
15
|
-
} from '
|
|
14
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
15
|
+
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
16
16
|
import type { VideoFrame } from './video_frame.js';
|
|
17
17
|
|
|
18
18
|
export class VideoSource {
|
package/src/video_stream.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { NewVideoStreamResponse, VideoRotation } from '@livekit/rtc-ffi-bindings';
|
|
5
|
+
import { NewVideoStreamRequest, VideoStreamType } from '@livekit/rtc-ffi-bindings';
|
|
4
6
|
import type { UnderlyingSource } from 'node:stream/web';
|
|
5
7
|
import type { FfiEvent } from './ffi_client.js';
|
|
6
8
|
import { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js';
|
|
7
|
-
import type { NewVideoStreamResponse, VideoRotation } from './proto/video_frame_pb.js';
|
|
8
|
-
import { NewVideoStreamRequest, VideoStreamType } from './proto/video_frame_pb.js';
|
|
9
9
|
import type { Track } from './track.js';
|
|
10
10
|
import { VideoFrame } from './video_frame.js';
|
|
11
11
|
|
|
@@ -18,6 +18,7 @@ export type VideoFrameEvent = {
|
|
|
18
18
|
class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {
|
|
19
19
|
private controller?: ReadableStreamDefaultController<VideoFrameEvent>;
|
|
20
20
|
private ffiHandle: FfiHandle;
|
|
21
|
+
private disposed = false;
|
|
21
22
|
|
|
22
23
|
constructor(track: Track) {
|
|
23
24
|
const req = new NewVideoStreamRequest({
|
|
@@ -65,6 +66,14 @@ class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {
|
|
|
65
66
|
case 'eos':
|
|
66
67
|
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
|
|
67
68
|
this.controller.close();
|
|
69
|
+
// Dispose the native handle so the FD is released on stream end,
|
|
70
|
+
// not just when cancel() is called explicitly by the consumer.
|
|
71
|
+
// Guard against double-dispose if cancel() is called after EOS
|
|
72
|
+
// while buffered frames are still in the ReadableStream queue.
|
|
73
|
+
if (!this.disposed) {
|
|
74
|
+
this.disposed = true;
|
|
75
|
+
this.ffiHandle.dispose();
|
|
76
|
+
}
|
|
68
77
|
break;
|
|
69
78
|
}
|
|
70
79
|
};
|
|
@@ -75,7 +84,10 @@ class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {
|
|
|
75
84
|
|
|
76
85
|
cancel() {
|
|
77
86
|
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
|
|
78
|
-
this.
|
|
87
|
+
if (!this.disposed) {
|
|
88
|
+
this.disposed = true;
|
|
89
|
+
this.ffiHandle.dispose();
|
|
90
|
+
}
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
package/Cargo.toml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
edition = "2021"
|
|
3
|
-
name = "rtc-node"
|
|
4
|
-
version = "0.0.1"
|
|
5
|
-
|
|
6
|
-
[lib]
|
|
7
|
-
crate-type = ["cdylib"]
|
|
8
|
-
|
|
9
|
-
[dependencies]
|
|
10
|
-
napi = { version = "2.12.2", default-features = false, features = [
|
|
11
|
-
"async",
|
|
12
|
-
"napi6",
|
|
13
|
-
] }
|
|
14
|
-
napi-derive = "2.12.2"
|
|
15
|
-
livekit-ffi = { path = "./rust-sdks/livekit-ffi" }
|
|
16
|
-
prost = "0.14"
|
|
17
|
-
prost-types = "0.14"
|
|
18
|
-
log = "0.4.20"
|
|
19
|
-
tokio = { version = "1.37.0", features = ["full"] }
|
|
20
|
-
|
|
21
|
-
[build-dependencies]
|
|
22
|
-
napi-build = "2.0.1"
|
|
23
|
-
|
|
24
|
-
[profile.release]
|
|
25
|
-
lto = true
|