@livekit/rtc-node 0.13.29 → 0.13.31

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 (77) hide show
  1. package/dist/audio_source.cjs +38 -19
  2. package/dist/audio_source.cjs.map +1 -1
  3. package/dist/audio_source.d.cts +12 -4
  4. package/dist/audio_source.d.ts +12 -4
  5. package/dist/audio_source.d.ts.map +1 -1
  6. package/dist/audio_source.js +38 -19
  7. package/dist/audio_source.js.map +1 -1
  8. package/dist/audio_stream-B4hGVcKz.d.cts +498 -0
  9. package/dist/audio_stream-DEG1JKge.d.ts +498 -0
  10. package/dist/audio_stream.cjs +21 -6
  11. package/dist/audio_stream.cjs.map +1 -1
  12. package/dist/audio_stream.d.cts +12 -22
  13. package/dist/audio_stream.d.ts +12 -22
  14. package/dist/audio_stream.d.ts.map +1 -1
  15. package/dist/audio_stream.js +19 -5
  16. package/dist/audio_stream.js.map +1 -1
  17. package/dist/data_streams/index.d.cts +1 -1
  18. package/dist/data_streams/index.d.ts +1 -1
  19. package/dist/data_streams/stream_reader.d.cts +1 -1
  20. package/dist/data_streams/stream_reader.d.ts +1 -1
  21. package/dist/data_streams/stream_writer.d.cts +1 -1
  22. package/dist/data_streams/stream_writer.d.ts +1 -1
  23. package/dist/data_streams/types.d.cts +1 -1
  24. package/dist/data_streams/types.d.ts +1 -1
  25. package/dist/frame_processor.cjs +6 -0
  26. package/dist/frame_processor.cjs.map +1 -1
  27. package/dist/frame_processor.d.cts +2 -0
  28. package/dist/frame_processor.d.ts +2 -0
  29. package/dist/frame_processor.d.ts.map +1 -1
  30. package/dist/frame_processor.js +6 -0
  31. package/dist/frame_processor.js.map +1 -1
  32. package/dist/index.d.cts +4 -7
  33. package/dist/index.d.ts +4 -7
  34. package/dist/participant.cjs +6 -0
  35. package/dist/participant.cjs.map +1 -1
  36. package/dist/participant.d.cts +14 -153
  37. package/dist/participant.d.ts +14 -153
  38. package/dist/participant.d.ts.map +1 -1
  39. package/dist/participant.js +6 -0
  40. package/dist/participant.js.map +1 -1
  41. package/dist/room.cjs +28 -0
  42. package/dist/room.cjs.map +1 -1
  43. package/dist/room.d.cts +12 -221
  44. package/dist/room.d.ts +12 -221
  45. package/dist/room.d.ts.map +1 -1
  46. package/dist/room.js +28 -0
  47. package/dist/room.js.map +1 -1
  48. package/dist/track.cjs +123 -0
  49. package/dist/track.cjs.map +1 -1
  50. package/dist/track.d.cts +15 -42
  51. package/dist/track.d.ts +15 -42
  52. package/dist/track.d.ts.map +1 -1
  53. package/dist/track.js +123 -0
  54. package/dist/track.js.map +1 -1
  55. package/dist/track_publication.d.cts +14 -47
  56. package/dist/track_publication.d.ts +14 -47
  57. package/dist/{stream_reader-CuG4b8Y-.d.cts → types-_PdPVish.d.cts} +40 -40
  58. package/dist/{stream_reader-CuG4b8Y-.d.ts → types-_PdPVish.d.ts} +40 -40
  59. package/dist/version.cjs +1 -1
  60. package/dist/version.cjs.map +1 -1
  61. package/dist/version.d.cts +1 -1
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/version.js.map +1 -1
  65. package/dist/video_stream.d.cts +12 -2
  66. package/dist/video_stream.d.ts +12 -2
  67. package/package.json +3 -3
  68. package/src/audio_source.test.ts +92 -0
  69. package/src/audio_source.ts +42 -18
  70. package/src/audio_stream.ts +28 -4
  71. package/src/audio_stream_room_lifecycle.test.ts +753 -0
  72. package/src/bun_runtime.test.ts +27 -0
  73. package/src/frame_processor.ts +4 -0
  74. package/src/participant.ts +17 -0
  75. package/src/room.ts +44 -0
  76. package/src/track.ts +141 -0
  77. package/src/version.ts +1 -1
@@ -0,0 +1,92 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { describe, expect, it } from 'vitest';
5
+ import { AudioFrame } from './audio_frame.js';
6
+ import { AudioSource } from './audio_source.js';
7
+
8
+ const SAMPLE_RATE = 24000;
9
+ const FRAME_MS = 20;
10
+ const SAMPLES = (SAMPLE_RATE * FRAME_MS) / 1000;
11
+ const QUEUE_MS = 200;
12
+
13
+ const makeFrame = () => new AudioFrame(new Int16Array(SAMPLES).fill(1000), SAMPLE_RATE, 1, SAMPLES);
14
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
15
+
16
+ async function pushAudio(source: AudioSource, durationMs: number) {
17
+ for (let i = 0; i < durationMs / FRAME_MS; i++) {
18
+ await source.captureFrame(makeFrame());
19
+ }
20
+ }
21
+
22
+ describe('AudioSource', () => {
23
+ it('waitForPlayout waits for the queued audio to drain', async () => {
24
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
25
+ await pushAudio(source, 600);
26
+ const pushedAt = Date.now();
27
+ await source.waitForPlayout();
28
+ // ~QUEUE_MS of audio is still buffered when the last capture returns
29
+ expect(Date.now() - pushedAt).toBeGreaterThanOrEqual(QUEUE_MS - 50);
30
+ await source.close();
31
+ });
32
+
33
+ it('waitForPlayout waits for drain after a capture gap fired the drain timer', async () => {
34
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
35
+ // a single frame followed by a gap longer than its duration: the internal
36
+ // drain timer fires and resolves the playout promise while the segment is
37
+ // still streaming
38
+ await source.captureFrame(makeFrame());
39
+ await sleep(FRAME_MS * 3);
40
+ await pushAudio(source, 600);
41
+ const pushedAt = Date.now();
42
+ await source.waitForPlayout();
43
+ expect(Date.now() - pushedAt).toBeGreaterThanOrEqual(QUEUE_MS - 50);
44
+ await source.close();
45
+ });
46
+
47
+ it('waitForPlayout waits for drain after a previous clearQueue', async () => {
48
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
49
+ // e.g. an interrupted turn: buffered audio is dropped, releasing the
50
+ // playout promise
51
+ await source.captureFrame(makeFrame());
52
+ source.clearQueue();
53
+ // the next turn must not consume the stale resolution
54
+ await pushAudio(source, 600);
55
+ const pushedAt = Date.now();
56
+ await source.waitForPlayout();
57
+ expect(Date.now() - pushedAt).toBeGreaterThanOrEqual(QUEUE_MS - 50);
58
+ await source.close();
59
+ });
60
+
61
+ it('waitForPlayout resolves promptly when interrupted by clearQueue', async () => {
62
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
63
+ await pushAudio(source, 600);
64
+ const playout = source.waitForPlayout();
65
+ source.clearQueue();
66
+ const clearedAt = Date.now();
67
+ await playout;
68
+ expect(Date.now() - clearedAt).toBeLessThan(50);
69
+ await source.close();
70
+ });
71
+
72
+ it('waitForPlayout resolves immediately when no audio is queued', async () => {
73
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
74
+ // nothing ever captured
75
+ const before = Date.now();
76
+ await source.waitForPlayout();
77
+ // fully drained (drain timer fired and released the waiter)
78
+ await pushAudio(source, 100);
79
+ await sleep(QUEUE_MS + 100);
80
+ await source.waitForPlayout();
81
+ expect(Date.now() - before).toBeLessThan(QUEUE_MS + 400);
82
+ await source.close();
83
+ });
84
+
85
+ it('close resolves a pending waitForPlayout', async () => {
86
+ const source = new AudioSource(SAMPLE_RATE, 1, QUEUE_MS);
87
+ await pushAudio(source, 600);
88
+ const playout = source.waitForPlayout();
89
+ await source.close();
90
+ await playout;
91
+ });
92
+ });
@@ -28,8 +28,9 @@ export class AudioSource {
28
28
  /** @internal */
29
29
  currentQueueSize: number;
30
30
  /** @internal */
31
- release = () => {};
32
- promise = this.newPromise();
31
+ promise?: Promise<void> = undefined;
32
+ /** @internal */
33
+ resolvePromise?: () => void = undefined;
33
34
  /** @internal */
34
35
  timeout?: ReturnType<typeof setTimeout> = undefined;
35
36
  /** @internal */
@@ -84,24 +85,41 @@ export class AudioSource {
84
85
  },
85
86
  });
86
87
 
87
- this.currentQueueSize = 0;
88
- this.release();
88
+ this.releaseWaiter();
89
89
  }
90
90
 
91
- /** @internal */
92
- async newPromise() {
93
- return new Promise<void>((resolve) => {
94
- this.release = resolve;
95
- });
96
- }
91
+ /**
92
+ * Resolve the pending waitForPlayout() promise (if any) and reset the queue
93
+ * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is
94
+ * discarded here and lazily re-created by the next captureFrame, so a later
95
+ * waitForPlayout() can never consume a stale resolution and report playout
96
+ * complete while audio is still queued.
97
+ * @internal
98
+ */
99
+ releaseWaiter = () => {
100
+ if (!this.promise) {
101
+ return;
102
+ }
97
103
 
98
- async waitForPlayout() {
99
- return this.promise.then(() => {
100
- this.lastCapture = 0;
101
- this.currentQueueSize = 0;
102
- this.promise = this.newPromise();
104
+ this.resolvePromise?.();
105
+ this.lastCapture = 0;
106
+ this.currentQueueSize = 0;
107
+ this.promise = undefined;
108
+ this.resolvePromise = undefined;
109
+ // cancel the drain timer (e.g. when released early by clearQueue), otherwise
110
+ // it would fire later and release the waiter of a subsequent segment
111
+ if (this.timeout) {
112
+ clearTimeout(this.timeout);
103
113
  this.timeout = undefined;
104
- });
114
+ }
115
+ };
116
+
117
+ async waitForPlayout() {
118
+ if (!this.promise) {
119
+ return;
120
+ }
121
+
122
+ await this.promise;
105
123
  }
106
124
 
107
125
  async captureFrame(frame: AudioFrame) {
@@ -124,7 +142,13 @@ export class AudioSource {
124
142
  clearTimeout(this.timeout);
125
143
  }
126
144
 
127
- this.timeout = setTimeout(this.release, this.currentQueueSize);
145
+ if (!this.promise) {
146
+ this.promise = new Promise<void>((resolve) => {
147
+ this.resolvePromise = resolve;
148
+ });
149
+ }
150
+
151
+ this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);
128
152
 
129
153
  const req = new CaptureAudioFrameRequest({
130
154
  sourceHandle: this.ffiHandle.handle,
@@ -152,7 +176,7 @@ export class AudioSource {
152
176
  this.timeout = undefined;
153
177
  }
154
178
  // Resolve any pending waitForPlayout() promise so callers don't hang.
155
- this.release();
179
+ this.releaseWaiter();
156
180
  this.ffiHandle.dispose();
157
181
  this.closed = true;
158
182
  }
@@ -13,6 +13,13 @@ import type { Track } from './track.js';
13
13
 
14
14
  export interface AudioStreamOptions {
15
15
  noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;
16
+ /**
17
+ * When the audio stream closes, whether to run the {@link FrameProcessor}'s
18
+ * `close()` method. If `false`, the processor is left open so it can be
19
+ * reused with another {@link AudioStream}. Only relevant when
20
+ * `noiseCancellation` is a {@link FrameProcessor}. Defaults to `true`.
21
+ */
22
+ autoCloseNoiseCancellation?: boolean;
16
23
  sampleRate?: number;
17
24
  numChannels?: number;
18
25
  frameSizeMs?: number;
@@ -24,26 +31,31 @@ export interface NoiseCancellationOptions {
24
31
  options: Record<string, any>;
25
32
  }
26
33
 
27
- class AudioStreamSource implements UnderlyingSource<AudioFrame> {
34
+ /** @internal */
35
+ export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
28
36
  private controller?: ReadableStreamDefaultController<AudioFrame>;
29
37
  private ffiHandle: FfiHandle;
30
38
  private disposed = false;
31
39
  private sampleRate: number;
32
40
  private numChannels: number;
33
41
  private legacyNcOptions?: NoiseCancellationOptions;
34
- private frameProcessor?: FrameProcessor<AudioFrame>;
42
+ private frameProcessor: FrameProcessor<AudioFrame> | null = null;
43
+ private autoCloseProcessor = true;
35
44
  private frameSizeMs?: number;
45
+ private track: Track;
36
46
 
37
47
  constructor(
38
48
  track: Track,
39
49
  sampleRateOrOptions?: number | AudioStreamOptions,
40
50
  numChannels?: number,
41
51
  ) {
52
+ this.track = track;
42
53
  if (sampleRateOrOptions !== undefined && typeof sampleRateOrOptions !== 'number') {
43
54
  this.sampleRate = sampleRateOrOptions.sampleRate ?? 48000;
44
55
  this.numChannels = sampleRateOrOptions.numChannels ?? 1;
45
56
  if (isFrameProcessor(sampleRateOrOptions.noiseCancellation)) {
46
57
  this.frameProcessor = sampleRateOrOptions.noiseCancellation;
58
+ this.autoCloseProcessor = sampleRateOrOptions.autoCloseNoiseCancellation ?? true;
47
59
  } else {
48
60
  this.legacyNcOptions = sampleRateOrOptions.noiseCancellation;
49
61
  }
@@ -77,6 +89,12 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
77
89
  this.ffiHandle = new FfiHandle(res.stream!.handle!.id!);
78
90
 
79
91
  FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent);
92
+ track.registerAudioStream(this);
93
+ }
94
+
95
+ /** @internal */
96
+ get processor(): FrameProcessor<AudioFrame> | null {
97
+ return this.frameProcessor;
80
98
  }
81
99
 
82
100
  private onEvent = (ev: FfiEvent) => {
@@ -113,8 +131,11 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
113
131
  // while buffered frames are still in the ReadableStream queue.
114
132
  if (!this.disposed) {
115
133
  this.disposed = true;
134
+ this.track.unregisterAudioStream(this);
116
135
  this.ffiHandle.dispose();
117
- this.frameProcessor?.close();
136
+ if (this.frameProcessor && this.autoCloseProcessor) {
137
+ this.frameProcessor.close();
138
+ }
118
139
  }
119
140
  break;
120
141
  }
@@ -128,10 +149,13 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
128
149
  FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
129
150
  if (!this.disposed) {
130
151
  this.disposed = true;
152
+ this.track.unregisterAudioStream(this);
131
153
  this.ffiHandle.dispose();
132
154
  // Also close the frame processor on cancel for symmetry with the EOS path,
133
155
  // so resources are released regardless of how the stream ends.
134
- this.frameProcessor?.close();
156
+ if (this.frameProcessor && this.autoCloseProcessor) {
157
+ this.frameProcessor.close();
158
+ }
135
159
  }
136
160
  }
137
161
  }