@livekit/rtc-node 0.13.24 → 0.13.26

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 (283) 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 +9 -5
  6. package/dist/audio_frame.cjs.map +1 -1
  7. package/dist/audio_frame.d.cts +2 -7
  8. package/dist/audio_frame.d.ts +2 -7
  9. package/dist/audio_frame.d.ts.map +1 -1
  10. package/dist/audio_frame.js +8 -4
  11. package/dist/audio_frame.js.map +1 -1
  12. package/dist/audio_mixer.cjs +10 -4
  13. package/dist/audio_mixer.cjs.map +1 -1
  14. package/dist/audio_mixer.d.cts +4 -7
  15. package/dist/audio_mixer.d.ts +4 -7
  16. package/dist/audio_mixer.d.ts.map +1 -1
  17. package/dist/audio_mixer.js +10 -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 +16 -6
  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 +15 -5
  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 +30 -15
  63. package/dist/ffi_client.cjs.map +1 -1
  64. package/dist/ffi_client.d.cts +6 -17
  65. package/dist/ffi_client.d.ts +6 -17
  66. package/dist/ffi_client.d.ts.map +1 -1
  67. package/dist/ffi_client.js +24 -6
  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/participant.cjs +146 -101
  84. package/dist/participant.cjs.map +1 -1
  85. package/dist/participant.d.cts +4 -16
  86. package/dist/participant.d.ts +4 -16
  87. package/dist/participant.d.ts.map +1 -1
  88. package/dist/participant.js +120 -75
  89. package/dist/participant.js.map +1 -1
  90. package/dist/room.cjs +116 -58
  91. package/dist/room.cjs.map +1 -1
  92. package/dist/room.d.cts +6 -15
  93. package/dist/room.d.ts +6 -15
  94. package/dist/room.d.ts.map +1 -1
  95. package/dist/room.js +113 -55
  96. package/dist/room.js.map +1 -1
  97. package/dist/rpc.cjs.map +1 -1
  98. package/dist/rpc.d.cts +1 -2
  99. package/dist/rpc.d.ts +1 -2
  100. package/dist/rpc.d.ts.map +1 -1
  101. package/dist/rpc.js.map +1 -1
  102. package/dist/{stream_reader-Ch2zvjU3.d.cts → stream_reader-CuG4b8Y-.d.cts} +1 -1
  103. package/dist/{stream_reader-DRyR29vo.d.ts → stream_reader-CuG4b8Y-.d.ts} +1 -1
  104. package/dist/track.cjs +3 -3
  105. package/dist/track.cjs.map +1 -1
  106. package/dist/track.d.cts +1 -14
  107. package/dist/track.d.ts +1 -14
  108. package/dist/track.d.ts.map +1 -1
  109. package/dist/track.js +1 -1
  110. package/dist/track.js.map +1 -1
  111. package/dist/track_publication.cjs +4 -4
  112. package/dist/track_publication.cjs.map +1 -1
  113. package/dist/track_publication.d.cts +1 -14
  114. package/dist/track_publication.d.ts +1 -14
  115. package/dist/track_publication.d.ts.map +1 -1
  116. package/dist/track_publication.js +2 -2
  117. package/dist/track_publication.js.map +1 -1
  118. package/dist/version.cjs +1 -1
  119. package/dist/version.cjs.map +1 -1
  120. package/dist/version.d.cts +1 -1
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/dist/version.js.map +1 -1
  124. package/dist/video_frame.cjs +42 -42
  125. package/dist/video_frame.cjs.map +1 -1
  126. package/dist/video_frame.d.cts +1 -6
  127. package/dist/video_frame.d.ts +1 -6
  128. package/dist/video_frame.d.ts.map +1 -1
  129. package/dist/video_frame.js +2 -2
  130. package/dist/video_frame.js.map +1 -1
  131. package/dist/video_source.cjs +5 -5
  132. package/dist/video_source.cjs.map +1 -1
  133. package/dist/video_source.d.cts +1 -14
  134. package/dist/video_source.d.ts +1 -14
  135. package/dist/video_source.d.ts.map +1 -1
  136. package/dist/video_source.js +2 -2
  137. package/dist/video_source.js.map +1 -1
  138. package/dist/video_stream.cjs +12 -4
  139. package/dist/video_stream.cjs.map +1 -1
  140. package/dist/video_stream.d.cts +1 -14
  141. package/dist/video_stream.d.ts +1 -14
  142. package/dist/video_stream.d.ts.map +1 -1
  143. package/dist/video_stream.js +10 -2
  144. package/dist/video_stream.js.map +1 -1
  145. package/package.json +7 -37
  146. package/src/audio_filter.ts +2 -2
  147. package/src/audio_frame.ts +17 -5
  148. package/src/audio_mixer.test.ts +102 -0
  149. package/src/audio_mixer.ts +10 -4
  150. package/src/audio_resampler.ts +14 -4
  151. package/src/audio_source.ts +13 -5
  152. package/src/audio_stream.ts +21 -6
  153. package/src/data_streams/stream_reader.ts +19 -1
  154. package/src/data_streams/types.ts +1 -1
  155. package/src/e2ee.ts +3 -3
  156. package/src/ffi_client.ts +33 -8
  157. package/src/frame_processor.ts +21 -0
  158. package/src/index.ts +6 -5
  159. package/src/participant.ts +117 -66
  160. package/src/room.ts +161 -69
  161. package/src/rpc.ts +1 -1
  162. package/src/tests/e2e.test.ts +179 -8
  163. package/src/track.ts +4 -4
  164. package/src/track_publication.ts +6 -6
  165. package/src/version.ts +1 -1
  166. package/src/video_frame.ts +3 -3
  167. package/src/video_source.ts +3 -3
  168. package/src/video_stream.ts +15 -3
  169. package/Cargo.toml +0 -25
  170. package/build.rs +0 -9
  171. package/dist/napi/native.cjs +0 -301
  172. package/dist/napi/native.d.cjs +0 -17
  173. package/dist/napi/native.d.cjs.map +0 -1
  174. package/dist/napi/native.d.d.cts +0 -21
  175. package/dist/napi/native.d.d.ts +0 -21
  176. package/dist/napi/native.d.js +0 -1
  177. package/dist/napi/native.d.js.map +0 -1
  178. package/dist/napi/native.d.ts +0 -19
  179. package/dist/napi/native.js +0 -15
  180. package/dist/native.cjs +0 -301
  181. package/dist/native.d.ts +0 -19
  182. package/dist/native.js +0 -15
  183. package/dist/proto/audio_frame_pb.cjs +0 -1357
  184. package/dist/proto/audio_frame_pb.cjs.map +0 -1
  185. package/dist/proto/audio_frame_pb.d.cts +0 -1183
  186. package/dist/proto/audio_frame_pb.d.ts +0 -1183
  187. package/dist/proto/audio_frame_pb.d.ts.map +0 -1
  188. package/dist/proto/audio_frame_pb.js +0 -1283
  189. package/dist/proto/audio_frame_pb.js.map +0 -1
  190. package/dist/proto/data_stream_pb.cjs +0 -1638
  191. package/dist/proto/data_stream_pb.cjs.map +0 -1
  192. package/dist/proto/data_stream_pb.d.cts +0 -1449
  193. package/dist/proto/data_stream_pb.d.ts +0 -1449
  194. package/dist/proto/data_stream_pb.d.ts.map +0 -1
  195. package/dist/proto/data_stream_pb.js +0 -1559
  196. package/dist/proto/data_stream_pb.js.map +0 -1
  197. package/dist/proto/e2ee_pb.cjs +0 -746
  198. package/dist/proto/e2ee_pb.cjs.map +0 -1
  199. package/dist/proto/e2ee_pb.d.cts +0 -653
  200. package/dist/proto/e2ee_pb.d.ts +0 -653
  201. package/dist/proto/e2ee_pb.d.ts.map +0 -1
  202. package/dist/proto/e2ee_pb.js +0 -696
  203. package/dist/proto/e2ee_pb.js.map +0 -1
  204. package/dist/proto/ffi_pb.cjs +0 -477
  205. package/dist/proto/ffi_pb.cjs.map +0 -1
  206. package/dist/proto/ffi_pb.d.cts +0 -1321
  207. package/dist/proto/ffi_pb.d.ts +0 -1321
  208. package/dist/proto/ffi_pb.d.ts.map +0 -1
  209. package/dist/proto/ffi_pb.js +0 -444
  210. package/dist/proto/ffi_pb.js.map +0 -1
  211. package/dist/proto/handle_pb.cjs +0 -53
  212. package/dist/proto/handle_pb.cjs.map +0 -1
  213. package/dist/proto/handle_pb.d.cts +0 -31
  214. package/dist/proto/handle_pb.d.ts +0 -31
  215. package/dist/proto/handle_pb.d.ts.map +0 -1
  216. package/dist/proto/handle_pb.js +0 -29
  217. package/dist/proto/handle_pb.js.map +0 -1
  218. package/dist/proto/participant_pb.cjs +0 -173
  219. package/dist/proto/participant_pb.cjs.map +0 -1
  220. package/dist/proto/participant_pb.d.cts +0 -218
  221. package/dist/proto/participant_pb.d.ts +0 -218
  222. package/dist/proto/participant_pb.d.ts.map +0 -1
  223. package/dist/proto/participant_pb.js +0 -145
  224. package/dist/proto/participant_pb.js.map +0 -1
  225. package/dist/proto/room_pb.cjs +0 -3374
  226. package/dist/proto/room_pb.cjs.map +0 -1
  227. package/dist/proto/room_pb.d.cts +0 -3028
  228. package/dist/proto/room_pb.d.ts +0 -3028
  229. package/dist/proto/room_pb.d.ts.map +0 -1
  230. package/dist/proto/room_pb.js +0 -3232
  231. package/dist/proto/room_pb.js.map +0 -1
  232. package/dist/proto/rpc_pb.cjs +0 -328
  233. package/dist/proto/rpc_pb.cjs.map +0 -1
  234. package/dist/proto/rpc_pb.d.cts +0 -267
  235. package/dist/proto/rpc_pb.d.ts +0 -267
  236. package/dist/proto/rpc_pb.d.ts.map +0 -1
  237. package/dist/proto/rpc_pb.js +0 -294
  238. package/dist/proto/rpc_pb.js.map +0 -1
  239. package/dist/proto/stats_pb.cjs +0 -1370
  240. package/dist/proto/stats_pb.cjs.map +0 -1
  241. package/dist/proto/stats_pb.d.cts +0 -1764
  242. package/dist/proto/stats_pb.d.ts +0 -1764
  243. package/dist/proto/stats_pb.d.ts.map +0 -1
  244. package/dist/proto/stats_pb.js +0 -1300
  245. package/dist/proto/stats_pb.js.map +0 -1
  246. package/dist/proto/track_pb.cjs +0 -630
  247. package/dist/proto/track_pb.cjs.map +0 -1
  248. package/dist/proto/track_pb.d.cts +0 -542
  249. package/dist/proto/track_pb.d.ts +0 -542
  250. package/dist/proto/track_pb.d.ts.map +0 -1
  251. package/dist/proto/track_pb.js +0 -584
  252. package/dist/proto/track_pb.js.map +0 -1
  253. package/dist/proto/track_publication_pb.cjs +0 -194
  254. package/dist/proto/track_publication_pb.cjs.map +0 -1
  255. package/dist/proto/track_publication_pb.d.cts +0 -135
  256. package/dist/proto/track_publication_pb.d.ts +0 -135
  257. package/dist/proto/track_publication_pb.d.ts.map +0 -1
  258. package/dist/proto/track_publication_pb.js +0 -164
  259. package/dist/proto/track_publication_pb.js.map +0 -1
  260. package/dist/proto/video_frame_pb.cjs +0 -723
  261. package/dist/proto/video_frame_pb.cjs.map +0 -1
  262. package/dist/proto/video_frame_pb.d.cts +0 -667
  263. package/dist/proto/video_frame_pb.d.ts +0 -667
  264. package/dist/proto/video_frame_pb.d.ts.map +0 -1
  265. package/dist/proto/video_frame_pb.js +0 -673
  266. package/dist/proto/video_frame_pb.js.map +0 -1
  267. package/src/lib.rs +0 -9
  268. package/src/napi/native.cjs +0 -301
  269. package/src/napi/native.d.ts +0 -19
  270. package/src/napi/native.js +0 -15
  271. package/src/nodejs.rs +0 -134
  272. package/src/proto/audio_frame_pb.ts +0 -2270
  273. package/src/proto/data_stream_pb.ts +0 -2674
  274. package/src/proto/e2ee_pb.ts +0 -1231
  275. package/src/proto/ffi_pb.ts +0 -1700
  276. package/src/proto/handle_pb.ts +0 -69
  277. package/src/proto/participant_pb.ts +0 -351
  278. package/src/proto/room_pb.ts +0 -5661
  279. package/src/proto/rpc_pb.ts +0 -539
  280. package/src/proto/stats_pb.ts +0 -3050
  281. package/src/proto/track_pb.ts +0 -1040
  282. package/src/proto/track_publication_pb.ts +0 -284
  283. package/src/proto/video_frame_pb.ts +0 -1236
@@ -4,7 +4,7 @@
4
4
  import { AccessToken } from 'livekit-server-sdk';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { setTimeout as delay } from 'node:timers/promises';
7
- import { afterAll, describe, expect, it } from 'vitest';
7
+ import { afterAll, describe, expect, it as itRaw } from 'vitest';
8
8
  import {
9
9
  AudioFrame,
10
10
  AudioSource,
@@ -20,6 +20,9 @@ import {
20
20
  dispose,
21
21
  } from '../index.js';
22
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
+
23
26
  const hasE2EEnv =
24
27
  !!process.env.LIVEKIT_URL && !!process.env.LIVEKIT_API_KEY && !!process.env.LIVEKIT_API_SECRET;
25
28
  const describeE2E = hasE2EEnv ? describe : describe.skip;
@@ -202,7 +205,7 @@ describeE2E('livekit-rtc e2e', () => {
202
205
  await dispose();
203
206
  });
204
207
 
205
- it.concurrent(
208
+ it(
206
209
  'connects to a room',
207
210
  async () => {
208
211
  const { roomName, rooms } = await connectTestRooms(1);
@@ -225,7 +228,7 @@ describeE2E('livekit-rtc e2e', () => {
225
228
  testTimeoutMs,
226
229
  );
227
230
 
228
- it.concurrent(
231
+ it(
229
232
  'connects multiple participants to the same room',
230
233
  async () => {
231
234
  const { roomName, rooms } = await connectTestRooms(2);
@@ -241,7 +244,7 @@ describeE2E('livekit-rtc e2e', () => {
241
244
  testTimeoutMs,
242
245
  );
243
246
 
244
- it.concurrent(
247
+ it(
245
248
  'emits participantDisconnected when a participant leaves',
246
249
  async () => {
247
250
  const { rooms } = await connectTestRooms(2);
@@ -267,7 +270,7 @@ describeE2E('livekit-rtc e2e', () => {
267
270
  testTimeoutMs,
268
271
  );
269
272
 
270
- it.concurrent(
273
+ it(
271
274
  'transfers audio between two participants (sine detection)',
272
275
  async () => {
273
276
  const cases = [
@@ -367,7 +370,7 @@ describeE2E('livekit-rtc e2e', () => {
367
370
  testTimeoutMs * 2,
368
371
  );
369
372
 
370
- it.concurrent(
373
+ it(
371
374
  'publishes and receives reliable data packets',
372
375
  async () => {
373
376
  const { rooms } = await connectTestRooms(2);
@@ -407,7 +410,7 @@ describeE2E('livekit-rtc e2e', () => {
407
410
  testTimeoutMs,
408
411
  );
409
412
 
410
- it.concurrent(
413
+ it(
411
414
  'sends and receives text and byte streams',
412
415
  async () => {
413
416
  const { rooms } = await connectTestRooms(2);
@@ -469,7 +472,7 @@ describeE2E('livekit-rtc e2e', () => {
469
472
  testTimeoutMs,
470
473
  );
471
474
 
472
- it.concurrent(
475
+ it(
473
476
  'invokes RPC methods and returns structured errors',
474
477
  async () => {
475
478
  const { rooms } = await connectTestRooms(2);
@@ -511,4 +514,172 @@ describeE2E('livekit-rtc e2e', () => {
511
514
  },
512
515
  testTimeoutMs * 2,
513
516
  );
517
+
518
+ it(
519
+ 'cleans up stream controllers when disconnecting during an active stream',
520
+ async () => {
521
+ const { rooms } = await connectTestRooms(2);
522
+ const [receivingRoom, sendingRoom] = rooms;
523
+ const topic = 'cleanup-stream-topic';
524
+
525
+ // Register a handler on the receiving side that will intentionally
526
+ // NOT fully consume the stream — simulating an abandoned transfer.
527
+ let readerReceived = false;
528
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
529
+ receivingRoom!.registerTextStreamHandler(topic, async (_reader, _sender) => {
530
+ readerReceived = true;
531
+ // Deliberately do not call reader.readAll() so the stream stays open
532
+ });
533
+
534
+ // Start sending a text stream but don't close it
535
+ const writer = await sendingRoom!.localParticipant!.streamText({ topic });
536
+ await writer.write('partial data');
537
+
538
+ // Wait for the receiving side to get the stream header
539
+ await waitFor(() => readerReceived, {
540
+ timeoutMs: 5000,
541
+ debugName: 'text stream header received',
542
+ });
543
+
544
+ // Disconnect the receiving room while the stream is still open.
545
+ // This should close the stream controller without throwing.
546
+ await receivingRoom!.disconnect();
547
+
548
+ // Also close the writer and disconnect the sender
549
+ await writer.close();
550
+ await sendingRoom!.disconnect();
551
+
552
+ // If we got here without hanging or throwing, the stream controller
553
+ // was properly cleaned up on disconnect.
554
+ },
555
+ testTimeoutMs,
556
+ );
557
+
558
+ it(
559
+ 'cleans up track publications when a remote participant disconnects',
560
+ async () => {
561
+ const { rooms } = await connectTestRooms(2);
562
+ const [stayingRoom, leavingRoom] = rooms;
563
+
564
+ // Publish a track from the leaving participant so its track publication
565
+ // will need to be cleaned up on disconnect.
566
+ const source = new AudioSource(48_000, 1);
567
+ const track = LocalAudioTrack.createAudioTrack('cleanup-test', source);
568
+ const options = new TrackPublishOptions();
569
+ options.source = TrackSource.SOURCE_MICROPHONE;
570
+ await leavingRoom!.localParticipant!.publishTrack(track, options);
571
+
572
+ // Wait for the staying room to see the track subscription
573
+ await waitFor(
574
+ () => {
575
+ const remote = stayingRoom!.remoteParticipants.get(
576
+ leavingRoom!.localParticipant!.identity,
577
+ );
578
+ return remote !== undefined && remote.trackPublications.size > 0;
579
+ },
580
+ { timeoutMs: 5000, debugName: 'track publication visible' },
581
+ );
582
+
583
+ // Capture a reference to the remote participant before disconnect
584
+ const remoteParticipant = stayingRoom!.remoteParticipants.get(
585
+ leavingRoom!.localParticipant!.identity,
586
+ )!;
587
+ expect(remoteParticipant.trackPublications.size).toBeGreaterThan(0);
588
+
589
+ // Listen for the disconnect event
590
+ const disconnected = waitForRoomEvent(
591
+ stayingRoom!,
592
+ RoomEvent.ParticipantDisconnected,
593
+ testTimeoutMs,
594
+ (p: { identity: string }) => p.identity,
595
+ );
596
+
597
+ await leavingRoom!.disconnect();
598
+ await disconnected;
599
+
600
+ // trackUnpublished events fire before participantDisconnected, so
601
+ // by this point all publications should already be removed and disposed.
602
+ expect(remoteParticipant.trackPublications.size).toBe(0);
603
+ expect(stayingRoom!.remoteParticipants.has(remoteParticipant.identity)).toBe(false);
604
+
605
+ await source.close();
606
+ await stayingRoom!.disconnect();
607
+ },
608
+ testTimeoutMs,
609
+ );
610
+
611
+ it(
612
+ 'cleans up resources when multiple participants disconnect simultaneously',
613
+ async () => {
614
+ // Connect 4 participants to stress-test concurrent disconnection cleanup
615
+ const { rooms } = await connectTestRooms(4);
616
+
617
+ // Publish a track from each participant to create track publications
618
+ const sources: AudioSource[] = [];
619
+ for (const room of rooms) {
620
+ const source = new AudioSource(48_000, 1);
621
+ sources.push(source);
622
+ const track = LocalAudioTrack.createAudioTrack('multi-cleanup', source);
623
+ const options = new TrackPublishOptions();
624
+ options.source = TrackSource.SOURCE_MICROPHONE;
625
+ await room.localParticipant!.publishTrack(track, options);
626
+ }
627
+
628
+ // Wait for all participants to see each other's tracks
629
+ await waitFor(
630
+ () =>
631
+ rooms.every(
632
+ (r) =>
633
+ r.remoteParticipants.size === 3 &&
634
+ [...r.remoteParticipants.values()].every((p) => p.trackPublications.size > 0),
635
+ ),
636
+ { timeoutMs: 5000, debugName: 'all tracks visible' },
637
+ );
638
+
639
+ // Register listeners before disconnecting so we can verify both
640
+ // RoomEvent.Disconnected and RoomEvent.ConnectionStateChanged fire
641
+ // for every room, even when disconnects race.
642
+ const disconnectedEvents = rooms.map((r) =>
643
+ waitForRoomEvent(r, RoomEvent.Disconnected, 3_000, (reason) => reason),
644
+ );
645
+ const connectionStateEvents = rooms.map((r) =>
646
+ waitForRoomEvent(r, RoomEvent.ConnectionStateChanged, 3_000, (state) => state),
647
+ );
648
+
649
+ // Disconnect all participants simultaneously
650
+ await Promise.all([...rooms.map((r) => r.disconnect()), ...sources.map((s) => s.close())]);
651
+
652
+ await Promise.all(disconnectedEvents);
653
+ const observedStates = await Promise.all(connectionStateEvents);
654
+
655
+ // Verify all rooms are disconnected and remote participant maps are empty
656
+ for (const room of rooms) {
657
+ expect(room.isConnected).toBe(false);
658
+ }
659
+ for (const state of observedStates) {
660
+ expect(state).toBe(ConnectionState.CONN_DISCONNECTED);
661
+ }
662
+ },
663
+ testTimeoutMs * 2,
664
+ );
665
+
666
+ it(
667
+ 'concurrent getSid() calls share a single listener and resolve consistently',
668
+ async () => {
669
+ const { rooms } = await connectTestRooms(1);
670
+ const room = rooms[0]!;
671
+
672
+ // Fire multiple concurrent getSid() calls — they should all resolve
673
+ // to the same SID without leaking event listeners.
674
+ const results = await Promise.all([room.getSid(), room.getSid(), room.getSid()]);
675
+
676
+ // All calls should return the same non-empty SID
677
+ expect(results[0]).toBeTruthy();
678
+ expect(results[1]).toBe(results[0]);
679
+ expect(results[2]).toBe(results[0]);
680
+
681
+ await room.disconnect();
682
+ },
683
+ testTimeoutMs,
684
+ );
514
685
  });
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.24";
1
+ export const SDK_VERSION = "0.13.26";
@@ -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
- }