@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.
Files changed (290) hide show
  1. package/dist/audio_filter.cjs +2 -2
  2. package/dist/audio_filter.cjs.map +1 -1
  3. package/dist/audio_filter.js +1 -1
  4. package/dist/audio_filter.js.map +1 -1
  5. package/dist/audio_frame.cjs +11 -7
  6. package/dist/audio_frame.cjs.map +1 -1
  7. package/dist/audio_frame.d.cts +3 -8
  8. package/dist/audio_frame.d.ts +3 -8
  9. package/dist/audio_frame.d.ts.map +1 -1
  10. package/dist/audio_frame.js +10 -6
  11. package/dist/audio_frame.js.map +1 -1
  12. package/dist/audio_mixer.cjs +11 -4
  13. package/dist/audio_mixer.cjs.map +1 -1
  14. package/dist/audio_mixer.d.cts +1 -6
  15. package/dist/audio_mixer.d.ts +1 -6
  16. package/dist/audio_mixer.d.ts.map +1 -1
  17. package/dist/audio_mixer.js +11 -4
  18. package/dist/audio_mixer.js.map +1 -1
  19. package/dist/audio_resampler.cjs +20 -12
  20. package/dist/audio_resampler.cjs.map +1 -1
  21. package/dist/audio_resampler.d.cts +6 -6
  22. package/dist/audio_resampler.d.ts +6 -6
  23. package/dist/audio_resampler.d.ts.map +1 -1
  24. package/dist/audio_resampler.js +12 -4
  25. package/dist/audio_resampler.js.map +1 -1
  26. package/dist/audio_source.cjs +11 -7
  27. package/dist/audio_source.cjs.map +1 -1
  28. package/dist/audio_source.d.cts +1 -7
  29. package/dist/audio_source.d.ts +1 -7
  30. package/dist/audio_source.d.ts.map +1 -1
  31. package/dist/audio_source.js +8 -3
  32. package/dist/audio_source.js.map +1 -1
  33. package/dist/audio_stream.cjs +17 -5
  34. package/dist/audio_stream.cjs.map +1 -1
  35. package/dist/audio_stream.d.cts +1 -14
  36. package/dist/audio_stream.d.ts +1 -14
  37. package/dist/audio_stream.d.ts.map +1 -1
  38. package/dist/audio_stream.js +16 -4
  39. package/dist/audio_stream.js.map +1 -1
  40. package/dist/data_streams/index.d.cts +2 -10
  41. package/dist/data_streams/index.d.ts +2 -10
  42. package/dist/data_streams/stream_reader.cjs +8 -0
  43. package/dist/data_streams/stream_reader.cjs.map +1 -1
  44. package/dist/data_streams/stream_reader.d.cts +2 -10
  45. package/dist/data_streams/stream_reader.d.ts +2 -10
  46. package/dist/data_streams/stream_reader.d.ts.map +1 -1
  47. package/dist/data_streams/stream_reader.js +8 -0
  48. package/dist/data_streams/stream_reader.js.map +1 -1
  49. package/dist/data_streams/stream_writer.d.cts +2 -10
  50. package/dist/data_streams/stream_writer.d.ts +2 -10
  51. package/dist/data_streams/types.cjs.map +1 -1
  52. package/dist/data_streams/types.d.cts +2 -10
  53. package/dist/data_streams/types.d.ts +2 -10
  54. package/dist/data_streams/types.d.ts.map +1 -1
  55. package/dist/e2ee.cjs +21 -21
  56. package/dist/e2ee.cjs.map +1 -1
  57. package/dist/e2ee.d.cts +1 -2
  58. package/dist/e2ee.d.ts +1 -2
  59. package/dist/e2ee.d.ts.map +1 -1
  60. package/dist/e2ee.js +2 -2
  61. package/dist/e2ee.js.map +1 -1
  62. package/dist/ffi_client.cjs +11 -12
  63. package/dist/ffi_client.cjs.map +1 -1
  64. package/dist/ffi_client.d.cts +3 -16
  65. package/dist/ffi_client.d.ts +3 -16
  66. package/dist/ffi_client.d.ts.map +1 -1
  67. package/dist/ffi_client.js +5 -3
  68. package/dist/ffi_client.js.map +1 -1
  69. package/dist/frame_processor.cjs +13 -2
  70. package/dist/frame_processor.cjs.map +1 -1
  71. package/dist/frame_processor.d.cts +5 -8
  72. package/dist/frame_processor.d.ts +5 -8
  73. package/dist/frame_processor.d.ts.map +1 -1
  74. package/dist/frame_processor.js +10 -1
  75. package/dist/frame_processor.js.map +1 -1
  76. package/dist/index.cjs +25 -23
  77. package/dist/index.cjs.map +1 -1
  78. package/dist/index.d.cts +3 -16
  79. package/dist/index.d.ts +3 -16
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +9 -7
  82. package/dist/index.js.map +1 -1
  83. package/dist/log.cjs +1 -10
  84. package/dist/log.cjs.map +1 -1
  85. package/dist/log.d.cts +4 -0
  86. package/dist/log.d.ts +4 -0
  87. package/dist/log.d.ts.map +1 -1
  88. package/dist/log.js +1 -10
  89. package/dist/log.js.map +1 -1
  90. package/dist/participant.cjs +38 -38
  91. package/dist/participant.cjs.map +1 -1
  92. package/dist/participant.d.cts +2 -15
  93. package/dist/participant.d.ts +2 -15
  94. package/dist/participant.d.ts.map +1 -1
  95. package/dist/participant.js +12 -12
  96. package/dist/participant.js.map +1 -1
  97. package/dist/room.cjs +48 -30
  98. package/dist/room.cjs.map +1 -1
  99. package/dist/room.d.cts +11 -16
  100. package/dist/room.d.ts +11 -16
  101. package/dist/room.d.ts.map +1 -1
  102. package/dist/room.js +44 -26
  103. package/dist/room.js.map +1 -1
  104. package/dist/rpc.cjs.map +1 -1
  105. package/dist/rpc.d.cts +1 -2
  106. package/dist/rpc.d.ts +1 -2
  107. package/dist/rpc.d.ts.map +1 -1
  108. package/dist/rpc.js.map +1 -1
  109. package/dist/{stream_reader-Ch2zvjU3.d.cts → stream_reader-CuG4b8Y-.d.cts} +1 -1
  110. package/dist/{stream_reader-DRyR29vo.d.ts → stream_reader-CuG4b8Y-.d.ts} +1 -1
  111. package/dist/track.cjs +3 -3
  112. package/dist/track.cjs.map +1 -1
  113. package/dist/track.d.cts +1 -14
  114. package/dist/track.d.ts +1 -14
  115. package/dist/track.d.ts.map +1 -1
  116. package/dist/track.js +1 -1
  117. package/dist/track.js.map +1 -1
  118. package/dist/track_publication.cjs +4 -4
  119. package/dist/track_publication.cjs.map +1 -1
  120. package/dist/track_publication.d.cts +1 -14
  121. package/dist/track_publication.d.ts +1 -14
  122. package/dist/track_publication.d.ts.map +1 -1
  123. package/dist/track_publication.js +2 -2
  124. package/dist/track_publication.js.map +1 -1
  125. package/dist/version.cjs +1 -1
  126. package/dist/version.cjs.map +1 -1
  127. package/dist/version.d.cts +1 -1
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/dist/version.js.map +1 -1
  131. package/dist/video_frame.cjs +42 -42
  132. package/dist/video_frame.cjs.map +1 -1
  133. package/dist/video_frame.d.cts +1 -6
  134. package/dist/video_frame.d.ts +1 -6
  135. package/dist/video_frame.d.ts.map +1 -1
  136. package/dist/video_frame.js +2 -2
  137. package/dist/video_frame.js.map +1 -1
  138. package/dist/video_source.cjs +5 -5
  139. package/dist/video_source.cjs.map +1 -1
  140. package/dist/video_source.d.cts +1 -14
  141. package/dist/video_source.d.ts +1 -14
  142. package/dist/video_source.d.ts.map +1 -1
  143. package/dist/video_source.js +2 -2
  144. package/dist/video_source.js.map +1 -1
  145. package/dist/video_stream.cjs +12 -4
  146. package/dist/video_stream.cjs.map +1 -1
  147. package/dist/video_stream.d.cts +1 -14
  148. package/dist/video_stream.d.ts +1 -14
  149. package/dist/video_stream.d.ts.map +1 -1
  150. package/dist/video_stream.js +10 -2
  151. package/dist/video_stream.js.map +1 -1
  152. package/package.json +9 -35
  153. package/src/audio_filter.ts +2 -2
  154. package/src/audio_frame.ts +26 -7
  155. package/src/audio_mixer.ts +15 -4
  156. package/src/audio_resampler.ts +14 -4
  157. package/src/audio_source.ts +13 -5
  158. package/src/audio_stream.ts +21 -5
  159. package/src/data_streams/stream_reader.ts +19 -1
  160. package/src/data_streams/types.ts +1 -1
  161. package/src/e2ee.ts +3 -3
  162. package/src/ffi_client.ts +7 -5
  163. package/src/frame_processor.ts +21 -0
  164. package/src/index.ts +6 -5
  165. package/src/log.ts +14 -10
  166. package/src/participant.ts +17 -17
  167. package/src/room.ts +65 -38
  168. package/src/rpc.ts +1 -1
  169. package/src/tests/e2e.test.ts +517 -0
  170. package/src/track.ts +4 -4
  171. package/src/track_publication.ts +6 -6
  172. package/src/version.ts +1 -1
  173. package/src/video_frame.ts +3 -3
  174. package/src/video_source.ts +3 -3
  175. package/src/video_stream.ts +15 -3
  176. package/Cargo.toml +0 -25
  177. package/build.rs +0 -9
  178. package/dist/napi/native.cjs +0 -301
  179. package/dist/napi/native.d.cjs +0 -17
  180. package/dist/napi/native.d.cjs.map +0 -1
  181. package/dist/napi/native.d.d.cts +0 -21
  182. package/dist/napi/native.d.d.ts +0 -21
  183. package/dist/napi/native.d.js +0 -1
  184. package/dist/napi/native.d.js.map +0 -1
  185. package/dist/napi/native.d.ts +0 -19
  186. package/dist/napi/native.js +0 -15
  187. package/dist/native.cjs +0 -301
  188. package/dist/native.d.ts +0 -19
  189. package/dist/native.js +0 -15
  190. package/dist/proto/audio_frame_pb.cjs +0 -1357
  191. package/dist/proto/audio_frame_pb.cjs.map +0 -1
  192. package/dist/proto/audio_frame_pb.d.cts +0 -1183
  193. package/dist/proto/audio_frame_pb.d.ts +0 -1183
  194. package/dist/proto/audio_frame_pb.d.ts.map +0 -1
  195. package/dist/proto/audio_frame_pb.js +0 -1283
  196. package/dist/proto/audio_frame_pb.js.map +0 -1
  197. package/dist/proto/data_stream_pb.cjs +0 -1638
  198. package/dist/proto/data_stream_pb.cjs.map +0 -1
  199. package/dist/proto/data_stream_pb.d.cts +0 -1449
  200. package/dist/proto/data_stream_pb.d.ts +0 -1449
  201. package/dist/proto/data_stream_pb.d.ts.map +0 -1
  202. package/dist/proto/data_stream_pb.js +0 -1559
  203. package/dist/proto/data_stream_pb.js.map +0 -1
  204. package/dist/proto/e2ee_pb.cjs +0 -746
  205. package/dist/proto/e2ee_pb.cjs.map +0 -1
  206. package/dist/proto/e2ee_pb.d.cts +0 -653
  207. package/dist/proto/e2ee_pb.d.ts +0 -653
  208. package/dist/proto/e2ee_pb.d.ts.map +0 -1
  209. package/dist/proto/e2ee_pb.js +0 -696
  210. package/dist/proto/e2ee_pb.js.map +0 -1
  211. package/dist/proto/ffi_pb.cjs +0 -477
  212. package/dist/proto/ffi_pb.cjs.map +0 -1
  213. package/dist/proto/ffi_pb.d.cts +0 -1321
  214. package/dist/proto/ffi_pb.d.ts +0 -1321
  215. package/dist/proto/ffi_pb.d.ts.map +0 -1
  216. package/dist/proto/ffi_pb.js +0 -444
  217. package/dist/proto/ffi_pb.js.map +0 -1
  218. package/dist/proto/handle_pb.cjs +0 -53
  219. package/dist/proto/handle_pb.cjs.map +0 -1
  220. package/dist/proto/handle_pb.d.cts +0 -31
  221. package/dist/proto/handle_pb.d.ts +0 -31
  222. package/dist/proto/handle_pb.d.ts.map +0 -1
  223. package/dist/proto/handle_pb.js +0 -29
  224. package/dist/proto/handle_pb.js.map +0 -1
  225. package/dist/proto/participant_pb.cjs +0 -173
  226. package/dist/proto/participant_pb.cjs.map +0 -1
  227. package/dist/proto/participant_pb.d.cts +0 -218
  228. package/dist/proto/participant_pb.d.ts +0 -218
  229. package/dist/proto/participant_pb.d.ts.map +0 -1
  230. package/dist/proto/participant_pb.js +0 -145
  231. package/dist/proto/participant_pb.js.map +0 -1
  232. package/dist/proto/room_pb.cjs +0 -3374
  233. package/dist/proto/room_pb.cjs.map +0 -1
  234. package/dist/proto/room_pb.d.cts +0 -3028
  235. package/dist/proto/room_pb.d.ts +0 -3028
  236. package/dist/proto/room_pb.d.ts.map +0 -1
  237. package/dist/proto/room_pb.js +0 -3232
  238. package/dist/proto/room_pb.js.map +0 -1
  239. package/dist/proto/rpc_pb.cjs +0 -328
  240. package/dist/proto/rpc_pb.cjs.map +0 -1
  241. package/dist/proto/rpc_pb.d.cts +0 -267
  242. package/dist/proto/rpc_pb.d.ts +0 -267
  243. package/dist/proto/rpc_pb.d.ts.map +0 -1
  244. package/dist/proto/rpc_pb.js +0 -294
  245. package/dist/proto/rpc_pb.js.map +0 -1
  246. package/dist/proto/stats_pb.cjs +0 -1370
  247. package/dist/proto/stats_pb.cjs.map +0 -1
  248. package/dist/proto/stats_pb.d.cts +0 -1764
  249. package/dist/proto/stats_pb.d.ts +0 -1764
  250. package/dist/proto/stats_pb.d.ts.map +0 -1
  251. package/dist/proto/stats_pb.js +0 -1300
  252. package/dist/proto/stats_pb.js.map +0 -1
  253. package/dist/proto/track_pb.cjs +0 -630
  254. package/dist/proto/track_pb.cjs.map +0 -1
  255. package/dist/proto/track_pb.d.cts +0 -542
  256. package/dist/proto/track_pb.d.ts +0 -542
  257. package/dist/proto/track_pb.d.ts.map +0 -1
  258. package/dist/proto/track_pb.js +0 -584
  259. package/dist/proto/track_pb.js.map +0 -1
  260. package/dist/proto/track_publication_pb.cjs +0 -194
  261. package/dist/proto/track_publication_pb.cjs.map +0 -1
  262. package/dist/proto/track_publication_pb.d.cts +0 -135
  263. package/dist/proto/track_publication_pb.d.ts +0 -135
  264. package/dist/proto/track_publication_pb.d.ts.map +0 -1
  265. package/dist/proto/track_publication_pb.js +0 -164
  266. package/dist/proto/track_publication_pb.js.map +0 -1
  267. package/dist/proto/video_frame_pb.cjs +0 -723
  268. package/dist/proto/video_frame_pb.cjs.map +0 -1
  269. package/dist/proto/video_frame_pb.d.cts +0 -667
  270. package/dist/proto/video_frame_pb.d.ts +0 -667
  271. package/dist/proto/video_frame_pb.d.ts.map +0 -1
  272. package/dist/proto/video_frame_pb.js +0 -673
  273. package/dist/proto/video_frame_pb.js.map +0 -1
  274. package/src/lib.rs +0 -9
  275. package/src/napi/native.cjs +0 -301
  276. package/src/napi/native.d.ts +0 -19
  277. package/src/napi/native.js +0 -15
  278. package/src/nodejs.rs +0 -134
  279. package/src/proto/audio_frame_pb.ts +0 -2270
  280. package/src/proto/data_stream_pb.ts +0 -2674
  281. package/src/proto/e2ee_pb.ts +0 -1231
  282. package/src/proto/ffi_pb.ts +0 -1700
  283. package/src/proto/handle_pb.ts +0 -69
  284. package/src/proto/participant_pb.ts +0 -351
  285. package/src/proto/room_pb.ts +0 -5661
  286. package/src/proto/rpc_pb.ts +0 -539
  287. package/src/proto/stats_pb.ts +0 -3050
  288. package/src/proto/track_pb.ts +0 -1040
  289. package/src/proto/track_publication_pb.ts +0 -284
  290. 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 './proto/track_pb.js';
14
- import { CreateAudioTrackRequest, CreateVideoTrackRequest } from './proto/track_pb.js';
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 {
@@ -1,17 +1,17 @@
1
1
  // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
- import { FfiClient } from './ffi_client.js';
5
- import { FfiHandle } from './napi/native.js';
6
- import type { EncryptionType } from './proto/e2ee_pb.js';
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 './proto/track_pb.js';
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.23";
1
+ export const SDK_VERSION = "0.13.25";
@@ -1,13 +1,13 @@
1
1
  // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
- import { FfiClient, FfiHandle, FfiRequest } from './ffi_client.js';
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 './proto/video_frame_pb.js';
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;
@@ -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 './proto/video_frame_pb.js';
8
+ } from '@livekit/rtc-ffi-bindings';
10
9
  import {
11
10
  CaptureVideoFrameRequest,
12
11
  NewVideoSourceRequest,
13
12
  VideoRotation,
14
13
  VideoSourceType,
15
- } from './proto/video_frame_pb.js';
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 {
@@ -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.ffiHandle.dispose();
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
package/build.rs DELETED
@@ -1,9 +0,0 @@
1
- // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
- //
3
- // SPDX-License-Identifier: Apache-2.0
4
-
5
- extern crate napi_build;
6
-
7
- fn main() {
8
- napi_build::setup();
9
- }