@livekit/rtc-node 0.13.30 → 0.13.32

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 (53) 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 → audio_stream-4M8XlO4-.d.cts} +25 -0
  9. package/dist/{audio_stream-DEG1JKge.d.ts → audio_stream-CTyN1Ldy.d.ts} +25 -0
  10. package/dist/audio_stream.cjs +31 -18
  11. package/dist/audio_stream.cjs.map +1 -1
  12. package/dist/audio_stream.d.cts +1 -1
  13. package/dist/audio_stream.d.ts +1 -1
  14. package/dist/audio_stream.d.ts.map +1 -1
  15. package/dist/audio_stream.js +31 -18
  16. package/dist/audio_stream.js.map +1 -1
  17. package/dist/index.d.cts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/participant.d.cts +1 -1
  20. package/dist/participant.d.ts +1 -1
  21. package/dist/room.cjs +1 -0
  22. package/dist/room.cjs.map +1 -1
  23. package/dist/room.d.cts +1 -1
  24. package/dist/room.d.ts +1 -1
  25. package/dist/room.d.ts.map +1 -1
  26. package/dist/room.js +1 -0
  27. package/dist/room.js.map +1 -1
  28. package/dist/track.cjs +16 -0
  29. package/dist/track.cjs.map +1 -1
  30. package/dist/track.d.cts +1 -1
  31. package/dist/track.d.ts +1 -1
  32. package/dist/track.d.ts.map +1 -1
  33. package/dist/track.js +16 -0
  34. package/dist/track.js.map +1 -1
  35. package/dist/track_publication.d.cts +1 -1
  36. package/dist/track_publication.d.ts +1 -1
  37. package/dist/version.cjs +1 -1
  38. package/dist/version.cjs.map +1 -1
  39. package/dist/version.d.cts +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/dist/video_stream.d.cts +1 -1
  44. package/dist/video_stream.d.ts +1 -1
  45. package/package.json +3 -3
  46. package/src/audio_source.test.ts +92 -0
  47. package/src/audio_source.ts +42 -18
  48. package/src/audio_stream.ts +37 -24
  49. package/src/audio_stream_room_lifecycle.test.ts +50 -3
  50. package/src/room.ts +4 -0
  51. package/src/tests/e2e.test.ts +137 -16
  52. package/src/track.ts +17 -0
  53. package/src/version.ts +1 -1
@@ -26,13 +26,36 @@ var import_ffi_client = require("./ffi_client.cjs");
26
26
  class AudioSource {
27
27
  constructor(sampleRate, numChannels, queueSize = 1e3) {
28
28
  /** @internal */
29
- this.release = () => {
30
- };
31
- this.promise = this.newPromise();
29
+ this.promise = void 0;
30
+ /** @internal */
31
+ this.resolvePromise = void 0;
32
32
  /** @internal */
33
33
  this.timeout = void 0;
34
34
  /** @internal */
35
35
  this.closed = false;
36
+ /**
37
+ * Resolve the pending waitForPlayout() promise (if any) and reset the queue
38
+ * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is
39
+ * discarded here and lazily re-created by the next captureFrame, so a later
40
+ * waitForPlayout() can never consume a stale resolution and report playout
41
+ * complete while audio is still queued.
42
+ * @internal
43
+ */
44
+ this.releaseWaiter = () => {
45
+ var _a;
46
+ if (!this.promise) {
47
+ return;
48
+ }
49
+ (_a = this.resolvePromise) == null ? void 0 : _a.call(this);
50
+ this.lastCapture = 0;
51
+ this.currentQueueSize = 0;
52
+ this.promise = void 0;
53
+ this.resolvePromise = void 0;
54
+ if (this.timeout) {
55
+ clearTimeout(this.timeout);
56
+ this.timeout = void 0;
57
+ }
58
+ };
36
59
  this.sampleRate = sampleRate;
37
60
  this.numChannels = numChannels;
38
61
  this.queueSize = queueSize;
@@ -69,22 +92,13 @@ class AudioSource {
69
92
  value: req
70
93
  }
71
94
  });
72
- this.currentQueueSize = 0;
73
- this.release();
74
- }
75
- /** @internal */
76
- async newPromise() {
77
- return new Promise((resolve) => {
78
- this.release = resolve;
79
- });
95
+ this.releaseWaiter();
80
96
  }
81
97
  async waitForPlayout() {
82
- return this.promise.then(() => {
83
- this.lastCapture = 0;
84
- this.currentQueueSize = 0;
85
- this.promise = this.newPromise();
86
- this.timeout = void 0;
87
- });
98
+ if (!this.promise) {
99
+ return;
100
+ }
101
+ await this.promise;
88
102
  }
89
103
  async captureFrame(frame) {
90
104
  if (this.closed) {
@@ -101,7 +115,12 @@ class AudioSource {
101
115
  if (this.timeout) {
102
116
  clearTimeout(this.timeout);
103
117
  }
104
- this.timeout = setTimeout(this.release, this.currentQueueSize);
118
+ if (!this.promise) {
119
+ this.promise = new Promise((resolve) => {
120
+ this.resolvePromise = resolve;
121
+ });
122
+ }
123
+ this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);
105
124
  const req = new import_rtc_ffi_bindings.CaptureAudioFrameRequest({
106
125
  sourceHandle: this.ffiHandle.handle,
107
126
  buffer: frame.protoInfo()
@@ -121,7 +140,7 @@ class AudioSource {
121
140
  clearTimeout(this.timeout);
122
141
  this.timeout = void 0;
123
142
  }
124
- this.release();
143
+ this.releaseWaiter();
125
144
  this.ffiHandle.dispose();
126
145
  this.closed = true;
127
146
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio_source.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n AudioSourceInfo,\n CaptureAudioFrameCallback,\n CaptureAudioFrameResponse,\n ClearAudioBufferResponse,\n NewAudioSourceResponse,\n} from '@livekit/rtc-ffi-bindings';\nimport {\n AudioSourceType,\n CaptureAudioFrameRequest,\n ClearAudioBufferRequest,\n FfiHandle,\n NewAudioSourceRequest,\n} from '@livekit/rtc-ffi-bindings';\nimport type { AudioFrame } from './audio_frame.js';\nimport { FfiClient } from './ffi_client.js';\n\nexport class AudioSource {\n /** @internal */\n info: AudioSourceInfo;\n /** @internal */\n ffiHandle: FfiHandle;\n /** @internal */\n lastCapture: number;\n /** @internal */\n currentQueueSize: number;\n /** @internal */\n release = () => {};\n promise = this.newPromise();\n /** @internal */\n timeout?: ReturnType<typeof setTimeout> = undefined;\n /** @internal */\n closed = false;\n\n sampleRate: number;\n numChannels: number;\n queueSize: number;\n\n constructor(sampleRate: number, numChannels: number, queueSize = 1000) {\n this.sampleRate = sampleRate;\n this.numChannels = numChannels;\n this.queueSize = queueSize;\n\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n\n const req = new NewAudioSourceRequest({\n type: AudioSourceType.AUDIO_SOURCE_NATIVE,\n sampleRate: sampleRate,\n numChannels: numChannels,\n queueSizeMs: queueSize,\n });\n\n const res = FfiClient.instance.request<NewAudioSourceResponse>({\n message: {\n case: 'newAudioSource',\n value: req,\n },\n });\n\n this.info = res.source!.info!;\n this.ffiHandle = new FfiHandle(res.source!.handle!.id!);\n }\n\n get queuedDuration(): number {\n return Math.max(\n this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture,\n 0,\n );\n }\n\n clearQueue() {\n const req = new ClearAudioBufferRequest({\n sourceHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<ClearAudioBufferResponse>({\n message: {\n case: 'clearAudioBuffer',\n value: req,\n },\n });\n\n this.currentQueueSize = 0;\n this.release();\n }\n\n /** @internal */\n async newPromise() {\n return new Promise<void>((resolve) => {\n this.release = resolve;\n });\n }\n\n async waitForPlayout() {\n return this.promise.then(() => {\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n this.promise = this.newPromise();\n this.timeout = undefined;\n });\n }\n\n async captureFrame(frame: AudioFrame) {\n if (this.closed) {\n throw new Error('AudioSource is closed');\n }\n\n if (frame.samplesPerChannel === 0) {\n return;\n }\n\n const now = Number(process.hrtime.bigint() / BigInt(1000000));\n const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture;\n const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n this.currentQueueSize = Math.max(this.currentQueueSize - elapsed, 0) + frameDurationMs;\n\n this.lastCapture = now;\n\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n\n this.timeout = setTimeout(this.release, this.currentQueueSize);\n\n const req = new CaptureAudioFrameRequest({\n sourceHandle: this.ffiHandle.handle,\n buffer: frame.protoInfo(),\n });\n\n const res = FfiClient.instance.request<CaptureAudioFrameResponse>({\n message: { case: 'captureAudioFrame', value: req },\n });\n\n const cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {\n return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;\n });\n\n if (cb.error) {\n throw new Error(cb.error);\n }\n }\n\n async close() {\n // Clear any pending playout timeout so its callback doesn't fire after\n // the handle is disposed, which would reference freed native state.\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n // Resolve any pending waitForPlayout() promise so callers don't hang.\n this.release();\n this.ffiHandle.dispose();\n this.closed = true;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/audio_source.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n AudioSourceInfo,\n CaptureAudioFrameCallback,\n CaptureAudioFrameResponse,\n ClearAudioBufferResponse,\n NewAudioSourceResponse,\n} from '@livekit/rtc-ffi-bindings';\nimport {\n AudioSourceType,\n CaptureAudioFrameRequest,\n ClearAudioBufferRequest,\n FfiHandle,\n NewAudioSourceRequest,\n} from '@livekit/rtc-ffi-bindings';\nimport type { AudioFrame } from './audio_frame.js';\nimport { FfiClient } from './ffi_client.js';\n\nexport class AudioSource {\n /** @internal */\n info: AudioSourceInfo;\n /** @internal */\n ffiHandle: FfiHandle;\n /** @internal */\n lastCapture: number;\n /** @internal */\n currentQueueSize: number;\n /** @internal */\n promise?: Promise<void> = undefined;\n /** @internal */\n resolvePromise?: () => void = undefined;\n /** @internal */\n timeout?: ReturnType<typeof setTimeout> = undefined;\n /** @internal */\n closed = false;\n\n sampleRate: number;\n numChannels: number;\n queueSize: number;\n\n constructor(sampleRate: number, numChannels: number, queueSize = 1000) {\n this.sampleRate = sampleRate;\n this.numChannels = numChannels;\n this.queueSize = queueSize;\n\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n\n const req = new NewAudioSourceRequest({\n type: AudioSourceType.AUDIO_SOURCE_NATIVE,\n sampleRate: sampleRate,\n numChannels: numChannels,\n queueSizeMs: queueSize,\n });\n\n const res = FfiClient.instance.request<NewAudioSourceResponse>({\n message: {\n case: 'newAudioSource',\n value: req,\n },\n });\n\n this.info = res.source!.info!;\n this.ffiHandle = new FfiHandle(res.source!.handle!.id!);\n }\n\n get queuedDuration(): number {\n return Math.max(\n this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture,\n 0,\n );\n }\n\n clearQueue() {\n const req = new ClearAudioBufferRequest({\n sourceHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<ClearAudioBufferResponse>({\n message: {\n case: 'clearAudioBuffer',\n value: req,\n },\n });\n\n this.releaseWaiter();\n }\n\n /**\n * Resolve the pending waitForPlayout() promise (if any) and reset the queue\n * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is\n * discarded here and lazily re-created by the next captureFrame, so a later\n * waitForPlayout() can never consume a stale resolution and report playout\n * complete while audio is still queued.\n * @internal\n */\n releaseWaiter = () => {\n if (!this.promise) {\n return;\n }\n\n this.resolvePromise?.();\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n this.promise = undefined;\n this.resolvePromise = undefined;\n // cancel the drain timer (e.g. when released early by clearQueue), otherwise\n // it would fire later and release the waiter of a subsequent segment\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n };\n\n async waitForPlayout() {\n if (!this.promise) {\n return;\n }\n\n await this.promise;\n }\n\n async captureFrame(frame: AudioFrame) {\n if (this.closed) {\n throw new Error('AudioSource is closed');\n }\n\n if (frame.samplesPerChannel === 0) {\n return;\n }\n\n const now = Number(process.hrtime.bigint() / BigInt(1000000));\n const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture;\n const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n this.currentQueueSize = Math.max(this.currentQueueSize - elapsed, 0) + frameDurationMs;\n\n this.lastCapture = now;\n\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n\n if (!this.promise) {\n this.promise = new Promise<void>((resolve) => {\n this.resolvePromise = resolve;\n });\n }\n\n this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);\n\n const req = new CaptureAudioFrameRequest({\n sourceHandle: this.ffiHandle.handle,\n buffer: frame.protoInfo(),\n });\n\n const res = FfiClient.instance.request<CaptureAudioFrameResponse>({\n message: { case: 'captureAudioFrame', value: req },\n });\n\n const cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {\n return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;\n });\n\n if (cb.error) {\n throw new Error(cb.error);\n }\n }\n\n async close() {\n // Clear any pending playout timeout so its callback doesn't fire after\n // the handle is disposed, which would reference freed native state.\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n // Resolve any pending waitForPlayout() promise so callers don't hang.\n this.releaseWaiter();\n this.ffiHandle.dispose();\n this.closed = true;\n }\n}\n"],"mappings":"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","names":[]}
@@ -11,8 +11,9 @@ declare class AudioSource {
11
11
  /** @internal */
12
12
  currentQueueSize: number;
13
13
  /** @internal */
14
- release: () => void;
15
- promise: Promise<void>;
14
+ promise?: Promise<void>;
15
+ /** @internal */
16
+ resolvePromise?: () => void;
16
17
  /** @internal */
17
18
  timeout?: ReturnType<typeof setTimeout>;
18
19
  /** @internal */
@@ -23,8 +24,15 @@ declare class AudioSource {
23
24
  constructor(sampleRate: number, numChannels: number, queueSize?: number);
24
25
  get queuedDuration(): number;
25
26
  clearQueue(): void;
26
- /** @internal */
27
- newPromise(): Promise<void>;
27
+ /**
28
+ * Resolve the pending waitForPlayout() promise (if any) and reset the queue
29
+ * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is
30
+ * discarded here and lazily re-created by the next captureFrame, so a later
31
+ * waitForPlayout() can never consume a stale resolution and report playout
32
+ * complete while audio is still queued.
33
+ * @internal
34
+ */
35
+ releaseWaiter: () => void;
28
36
  waitForPlayout(): Promise<void>;
29
37
  captureFrame(frame: AudioFrame): Promise<void>;
30
38
  close(): Promise<void>;
@@ -11,8 +11,9 @@ declare class AudioSource {
11
11
  /** @internal */
12
12
  currentQueueSize: number;
13
13
  /** @internal */
14
- release: () => void;
15
- promise: Promise<void>;
14
+ promise?: Promise<void>;
15
+ /** @internal */
16
+ resolvePromise?: () => void;
16
17
  /** @internal */
17
18
  timeout?: ReturnType<typeof setTimeout>;
18
19
  /** @internal */
@@ -23,8 +24,15 @@ declare class AudioSource {
23
24
  constructor(sampleRate: number, numChannels: number, queueSize?: number);
24
25
  get queuedDuration(): number;
25
26
  clearQueue(): void;
26
- /** @internal */
27
- newPromise(): Promise<void>;
27
+ /**
28
+ * Resolve the pending waitForPlayout() promise (if any) and reset the queue
29
+ * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is
30
+ * discarded here and lazily re-created by the next captureFrame, so a later
31
+ * waitForPlayout() can never consume a stale resolution and report playout
32
+ * complete while audio is still queued.
33
+ * @internal
34
+ */
35
+ releaseWaiter: () => void;
28
36
  waitForPlayout(): Promise<void>;
29
37
  captureFrame(frame: AudioFrame): Promise<void>;
30
38
  close(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"audio_source.d.ts","sourceRoot":"","sources":["../src/audio_source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EAKhB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAIL,SAAS,EAEV,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,qBAAa,WAAW;IACtB,gBAAgB;IAChB,IAAI,EAAE,eAAe,CAAC;IACtB,gBAAgB;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,gBAAgB;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB;IAChB,OAAO,aAAY;IACnB,OAAO,gBAAqB;IAC5B,gBAAgB;IAChB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAa;IACpD,gBAAgB;IAChB,MAAM,UAAS;IAEf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;gBAEN,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,SAAO;IA0BrE,IAAI,cAAc,IAAI,MAAM,CAK3B;IAED,UAAU;IAgBV,gBAAgB;IACV,UAAU;IAMV,cAAc;IASd,YAAY,CAAC,KAAK,EAAE,UAAU;IAwC9B,KAAK;CAYZ"}
1
+ {"version":3,"file":"audio_source.d.ts","sourceRoot":"","sources":["../src/audio_source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EAKhB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAIL,SAAS,EAEV,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,qBAAa,WAAW;IACtB,gBAAgB;IAChB,IAAI,EAAE,eAAe,CAAC;IACtB,gBAAgB;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,gBAAgB;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAa;IACpC,gBAAgB;IAChB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAa;IACxC,gBAAgB;IAChB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAa;IACpD,gBAAgB;IAChB,MAAM,UAAS;IAEf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;gBAEN,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,SAAO;IA0BrE,IAAI,cAAc,IAAI,MAAM,CAK3B;IAED,UAAU;IAeV;;;;;;;OAOG;IACH,aAAa,aAgBX;IAEI,cAAc;IAQd,YAAY,CAAC,KAAK,EAAE,UAAU;IA8C9B,KAAK;CAYZ"}
@@ -9,13 +9,36 @@ import { FfiClient } from "./ffi_client.js";
9
9
  class AudioSource {
10
10
  constructor(sampleRate, numChannels, queueSize = 1e3) {
11
11
  /** @internal */
12
- this.release = () => {
13
- };
14
- this.promise = this.newPromise();
12
+ this.promise = void 0;
13
+ /** @internal */
14
+ this.resolvePromise = void 0;
15
15
  /** @internal */
16
16
  this.timeout = void 0;
17
17
  /** @internal */
18
18
  this.closed = false;
19
+ /**
20
+ * Resolve the pending waitForPlayout() promise (if any) and reset the queue
21
+ * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is
22
+ * discarded here and lazily re-created by the next captureFrame, so a later
23
+ * waitForPlayout() can never consume a stale resolution and report playout
24
+ * complete while audio is still queued.
25
+ * @internal
26
+ */
27
+ this.releaseWaiter = () => {
28
+ var _a;
29
+ if (!this.promise) {
30
+ return;
31
+ }
32
+ (_a = this.resolvePromise) == null ? void 0 : _a.call(this);
33
+ this.lastCapture = 0;
34
+ this.currentQueueSize = 0;
35
+ this.promise = void 0;
36
+ this.resolvePromise = void 0;
37
+ if (this.timeout) {
38
+ clearTimeout(this.timeout);
39
+ this.timeout = void 0;
40
+ }
41
+ };
19
42
  this.sampleRate = sampleRate;
20
43
  this.numChannels = numChannels;
21
44
  this.queueSize = queueSize;
@@ -52,22 +75,13 @@ class AudioSource {
52
75
  value: req
53
76
  }
54
77
  });
55
- this.currentQueueSize = 0;
56
- this.release();
57
- }
58
- /** @internal */
59
- async newPromise() {
60
- return new Promise((resolve) => {
61
- this.release = resolve;
62
- });
78
+ this.releaseWaiter();
63
79
  }
64
80
  async waitForPlayout() {
65
- return this.promise.then(() => {
66
- this.lastCapture = 0;
67
- this.currentQueueSize = 0;
68
- this.promise = this.newPromise();
69
- this.timeout = void 0;
70
- });
81
+ if (!this.promise) {
82
+ return;
83
+ }
84
+ await this.promise;
71
85
  }
72
86
  async captureFrame(frame) {
73
87
  if (this.closed) {
@@ -84,7 +98,12 @@ class AudioSource {
84
98
  if (this.timeout) {
85
99
  clearTimeout(this.timeout);
86
100
  }
87
- this.timeout = setTimeout(this.release, this.currentQueueSize);
101
+ if (!this.promise) {
102
+ this.promise = new Promise((resolve) => {
103
+ this.resolvePromise = resolve;
104
+ });
105
+ }
106
+ this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);
88
107
  const req = new CaptureAudioFrameRequest({
89
108
  sourceHandle: this.ffiHandle.handle,
90
109
  buffer: frame.protoInfo()
@@ -104,7 +123,7 @@ class AudioSource {
104
123
  clearTimeout(this.timeout);
105
124
  this.timeout = void 0;
106
125
  }
107
- this.release();
126
+ this.releaseWaiter();
108
127
  this.ffiHandle.dispose();
109
128
  this.closed = true;
110
129
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio_source.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n AudioSourceInfo,\n CaptureAudioFrameCallback,\n CaptureAudioFrameResponse,\n ClearAudioBufferResponse,\n NewAudioSourceResponse,\n} from '@livekit/rtc-ffi-bindings';\nimport {\n AudioSourceType,\n CaptureAudioFrameRequest,\n ClearAudioBufferRequest,\n FfiHandle,\n NewAudioSourceRequest,\n} from '@livekit/rtc-ffi-bindings';\nimport type { AudioFrame } from './audio_frame.js';\nimport { FfiClient } from './ffi_client.js';\n\nexport class AudioSource {\n /** @internal */\n info: AudioSourceInfo;\n /** @internal */\n ffiHandle: FfiHandle;\n /** @internal */\n lastCapture: number;\n /** @internal */\n currentQueueSize: number;\n /** @internal */\n release = () => {};\n promise = this.newPromise();\n /** @internal */\n timeout?: ReturnType<typeof setTimeout> = undefined;\n /** @internal */\n closed = false;\n\n sampleRate: number;\n numChannels: number;\n queueSize: number;\n\n constructor(sampleRate: number, numChannels: number, queueSize = 1000) {\n this.sampleRate = sampleRate;\n this.numChannels = numChannels;\n this.queueSize = queueSize;\n\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n\n const req = new NewAudioSourceRequest({\n type: AudioSourceType.AUDIO_SOURCE_NATIVE,\n sampleRate: sampleRate,\n numChannels: numChannels,\n queueSizeMs: queueSize,\n });\n\n const res = FfiClient.instance.request<NewAudioSourceResponse>({\n message: {\n case: 'newAudioSource',\n value: req,\n },\n });\n\n this.info = res.source!.info!;\n this.ffiHandle = new FfiHandle(res.source!.handle!.id!);\n }\n\n get queuedDuration(): number {\n return Math.max(\n this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture,\n 0,\n );\n }\n\n clearQueue() {\n const req = new ClearAudioBufferRequest({\n sourceHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<ClearAudioBufferResponse>({\n message: {\n case: 'clearAudioBuffer',\n value: req,\n },\n });\n\n this.currentQueueSize = 0;\n this.release();\n }\n\n /** @internal */\n async newPromise() {\n return new Promise<void>((resolve) => {\n this.release = resolve;\n });\n }\n\n async waitForPlayout() {\n return this.promise.then(() => {\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n this.promise = this.newPromise();\n this.timeout = undefined;\n });\n }\n\n async captureFrame(frame: AudioFrame) {\n if (this.closed) {\n throw new Error('AudioSource is closed');\n }\n\n if (frame.samplesPerChannel === 0) {\n return;\n }\n\n const now = Number(process.hrtime.bigint() / BigInt(1000000));\n const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture;\n const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n this.currentQueueSize = Math.max(this.currentQueueSize - elapsed, 0) + frameDurationMs;\n\n this.lastCapture = now;\n\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n\n this.timeout = setTimeout(this.release, this.currentQueueSize);\n\n const req = new CaptureAudioFrameRequest({\n sourceHandle: this.ffiHandle.handle,\n buffer: frame.protoInfo(),\n });\n\n const res = FfiClient.instance.request<CaptureAudioFrameResponse>({\n message: { case: 'captureAudioFrame', value: req },\n });\n\n const cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {\n return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;\n });\n\n if (cb.error) {\n throw new Error(cb.error);\n }\n }\n\n async close() {\n // Clear any pending playout timeout so its callback doesn't fire after\n // the handle is disposed, which would reference freed native state.\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n // Resolve any pending waitForPlayout() promise so callers don't hang.\n this.release();\n this.ffiHandle.dispose();\n this.closed = true;\n }\n}\n"],"mappings":"AAUA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iBAAiB;AAEnB,MAAM,YAAY;AAAA,EAqBvB,YAAY,YAAoB,aAAqB,YAAY,KAAM;AAXvE;AAAA,mBAAU,MAAM;AAAA,IAAC;AACjB,mBAAU,KAAK,WAAW;AAE1B;AAAA,mBAA0C;AAE1C;AAAA,kBAAS;AAOP,SAAK,aAAa;AAClB,SAAK,cAAc;AACnB,SAAK,YAAY;AAEjB,SAAK,cAAc;AACnB,SAAK,mBAAmB;AAExB,UAAM,MAAM,IAAI,sBAAsB;AAAA,MACpC,MAAM,gBAAgB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,aAAa;AAAA,IACf,CAAC;AAED,UAAM,MAAM,UAAU,SAAS,QAAgC;AAAA,MAC7D,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,SAAK,OAAO,IAAI,OAAQ;AACxB,SAAK,YAAY,IAAI,UAAU,IAAI,OAAQ,OAAQ,EAAG;AAAA,EACxD;AAAA,EAEA,IAAI,iBAAyB;AAC3B,WAAO,KAAK;AAAA,MACV,KAAK,mBAAmB,OAAO,QAAQ,OAAO,OAAO,IAAI,OAAO,GAAO,CAAC,IAAI,KAAK;AAAA,MACjF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,aAAa;AACX,UAAM,MAAM,IAAI,wBAAwB;AAAA,MACtC,cAAc,KAAK,UAAU;AAAA,IAC/B,CAAC;AAED,cAAU,SAAS,QAAkC;AAAA,MACnD,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,SAAK,mBAAmB;AACxB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,MAAM,aAAa;AACjB,WAAO,IAAI,QAAc,CAAC,YAAY;AACpC,WAAK,UAAU;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,iBAAiB;AACrB,WAAO,KAAK,QAAQ,KAAK,MAAM;AAC7B,WAAK,cAAc;AACnB,WAAK,mBAAmB;AACxB,WAAK,UAAU,KAAK,WAAW;AAC/B,WAAK,UAAU;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,aAAa,OAAmB;AACpC,QAAI,KAAK,QAAQ;AACf,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AAEA,QAAI,MAAM,sBAAsB,GAAG;AACjC;AAAA,IACF;AAEA,UAAM,MAAM,OAAO,QAAQ,OAAO,OAAO,IAAI,OAAO,GAAO,CAAC;AAC5D,UAAM,UAAU,KAAK,gBAAgB,IAAI,IAAI,MAAM,KAAK;AACxD,UAAM,kBAAmB,MAAM,oBAAoB,MAAM,aAAc;AACvE,SAAK,mBAAmB,KAAK,IAAI,KAAK,mBAAmB,SAAS,CAAC,IAAI;AAEvE,SAAK,cAAc;AAEnB,QAAI,KAAK,SAAS;AAChB,mBAAa,KAAK,OAAO;AAAA,IAC3B;AAEA,SAAK,UAAU,WAAW,KAAK,SAAS,KAAK,gBAAgB;AAE7D,UAAM,MAAM,IAAI,yBAAyB;AAAA,MACvC,cAAc,KAAK,UAAU;AAAA,MAC7B,QAAQ,MAAM,UAAU;AAAA,IAC1B,CAAC;AAED,UAAM,MAAM,UAAU,SAAS,QAAmC;AAAA,MAChE,SAAS,EAAE,MAAM,qBAAqB,OAAO,IAAI;AAAA,IACnD,CAAC;AAED,UAAM,KAAK,MAAM,UAAU,SAAS,QAAmC,CAAC,OAAO;AAC7E,aAAO,GAAG,QAAQ,QAAQ,uBAAuB,GAAG,QAAQ,MAAM,WAAW,IAAI;AAAA,IACnF,CAAC;AAED,QAAI,GAAG,OAAO;AACZ,YAAM,IAAI,MAAM,GAAG,KAAK;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ;AAGZ,QAAI,KAAK,SAAS;AAChB,mBAAa,KAAK,OAAO;AACzB,WAAK,UAAU;AAAA,IACjB;AAEA,SAAK,QAAQ;AACb,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/audio_source.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n AudioSourceInfo,\n CaptureAudioFrameCallback,\n CaptureAudioFrameResponse,\n ClearAudioBufferResponse,\n NewAudioSourceResponse,\n} from '@livekit/rtc-ffi-bindings';\nimport {\n AudioSourceType,\n CaptureAudioFrameRequest,\n ClearAudioBufferRequest,\n FfiHandle,\n NewAudioSourceRequest,\n} from '@livekit/rtc-ffi-bindings';\nimport type { AudioFrame } from './audio_frame.js';\nimport { FfiClient } from './ffi_client.js';\n\nexport class AudioSource {\n /** @internal */\n info: AudioSourceInfo;\n /** @internal */\n ffiHandle: FfiHandle;\n /** @internal */\n lastCapture: number;\n /** @internal */\n currentQueueSize: number;\n /** @internal */\n promise?: Promise<void> = undefined;\n /** @internal */\n resolvePromise?: () => void = undefined;\n /** @internal */\n timeout?: ReturnType<typeof setTimeout> = undefined;\n /** @internal */\n closed = false;\n\n sampleRate: number;\n numChannels: number;\n queueSize: number;\n\n constructor(sampleRate: number, numChannels: number, queueSize = 1000) {\n this.sampleRate = sampleRate;\n this.numChannels = numChannels;\n this.queueSize = queueSize;\n\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n\n const req = new NewAudioSourceRequest({\n type: AudioSourceType.AUDIO_SOURCE_NATIVE,\n sampleRate: sampleRate,\n numChannels: numChannels,\n queueSizeMs: queueSize,\n });\n\n const res = FfiClient.instance.request<NewAudioSourceResponse>({\n message: {\n case: 'newAudioSource',\n value: req,\n },\n });\n\n this.info = res.source!.info!;\n this.ffiHandle = new FfiHandle(res.source!.handle!.id!);\n }\n\n get queuedDuration(): number {\n return Math.max(\n this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture,\n 0,\n );\n }\n\n clearQueue() {\n const req = new ClearAudioBufferRequest({\n sourceHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<ClearAudioBufferResponse>({\n message: {\n case: 'clearAudioBuffer',\n value: req,\n },\n });\n\n this.releaseWaiter();\n }\n\n /**\n * Resolve the pending waitForPlayout() promise (if any) and reset the queue\n * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is\n * discarded here and lazily re-created by the next captureFrame, so a later\n * waitForPlayout() can never consume a stale resolution and report playout\n * complete while audio is still queued.\n * @internal\n */\n releaseWaiter = () => {\n if (!this.promise) {\n return;\n }\n\n this.resolvePromise?.();\n this.lastCapture = 0;\n this.currentQueueSize = 0;\n this.promise = undefined;\n this.resolvePromise = undefined;\n // cancel the drain timer (e.g. when released early by clearQueue), otherwise\n // it would fire later and release the waiter of a subsequent segment\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n };\n\n async waitForPlayout() {\n if (!this.promise) {\n return;\n }\n\n await this.promise;\n }\n\n async captureFrame(frame: AudioFrame) {\n if (this.closed) {\n throw new Error('AudioSource is closed');\n }\n\n if (frame.samplesPerChannel === 0) {\n return;\n }\n\n const now = Number(process.hrtime.bigint() / BigInt(1000000));\n const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture;\n const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n this.currentQueueSize = Math.max(this.currentQueueSize - elapsed, 0) + frameDurationMs;\n\n this.lastCapture = now;\n\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n\n if (!this.promise) {\n this.promise = new Promise<void>((resolve) => {\n this.resolvePromise = resolve;\n });\n }\n\n this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);\n\n const req = new CaptureAudioFrameRequest({\n sourceHandle: this.ffiHandle.handle,\n buffer: frame.protoInfo(),\n });\n\n const res = FfiClient.instance.request<CaptureAudioFrameResponse>({\n message: { case: 'captureAudioFrame', value: req },\n });\n\n const cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {\n return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;\n });\n\n if (cb.error) {\n throw new Error(cb.error);\n }\n }\n\n async close() {\n // Clear any pending playout timeout so its callback doesn't fire after\n // the handle is disposed, which would reference freed native state.\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n // Resolve any pending waitForPlayout() promise so callers don't hang.\n this.releaseWaiter();\n this.ffiHandle.dispose();\n this.closed = true;\n }\n}\n"],"mappings":"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","names":[]}
@@ -420,6 +420,17 @@ declare abstract class Track {
420
420
  registerAudioStream(stream: AudioStreamSource): void;
421
421
  /** @internal */
422
422
  unregisterAudioStream(stream: AudioStreamSource): void;
423
+ /**
424
+ * End every {@link AudioStream} attached to this track.
425
+ *
426
+ * @remarks
427
+ * The FFI keeps feeding an audio stream until the stream's own handle is
428
+ * dropped, so streams on a track that is no longer subscribed would otherwise
429
+ * go on delivering audio for the lifetime of the process.
430
+ *
431
+ * @internal
432
+ */
433
+ closeAudioStreams(): void;
423
434
  private iterateStreams;
424
435
  private pushProcessorMetadataToStream;
425
436
  get sid(): string | undefined;
@@ -486,6 +497,20 @@ declare class AudioStreamSource implements UnderlyingSource<AudioFrame> {
486
497
  get processor(): FrameProcessor<AudioFrame> | null;
487
498
  private onEvent;
488
499
  start(controller: ReadableStreamDefaultController<AudioFrame>): void;
500
+ /**
501
+ * Detach from the FFI and release resources: on `eos`, on `cancel()`, or
502
+ * because the track went away (e.g. it was unsubscribed — which never
503
+ * produces an `eos`, so without this the stream would keep delivering
504
+ * frames). Already-buffered frames stay readable and the consumer sees `done`
505
+ * after draining them.
506
+ *
507
+ * @remarks
508
+ * Idempotent, so `eos` arriving after a `cancel()` (or vice versa) doesn't
509
+ * double-dispose the handle while buffered frames are still queued.
510
+ *
511
+ * @internal
512
+ */
513
+ teardown(): void;
489
514
  cancel(): void;
490
515
  }
491
516
  declare class AudioStream extends ReadableStream<AudioFrame> {
@@ -420,6 +420,17 @@ declare abstract class Track {
420
420
  registerAudioStream(stream: AudioStreamSource): void;
421
421
  /** @internal */
422
422
  unregisterAudioStream(stream: AudioStreamSource): void;
423
+ /**
424
+ * End every {@link AudioStream} attached to this track.
425
+ *
426
+ * @remarks
427
+ * The FFI keeps feeding an audio stream until the stream's own handle is
428
+ * dropped, so streams on a track that is no longer subscribed would otherwise
429
+ * go on delivering audio for the lifetime of the process.
430
+ *
431
+ * @internal
432
+ */
433
+ closeAudioStreams(): void;
423
434
  private iterateStreams;
424
435
  private pushProcessorMetadataToStream;
425
436
  get sid(): string | undefined;
@@ -486,6 +497,20 @@ declare class AudioStreamSource implements UnderlyingSource<AudioFrame> {
486
497
  get processor(): FrameProcessor<AudioFrame> | null;
487
498
  private onEvent;
488
499
  start(controller: ReadableStreamDefaultController<AudioFrame>): void;
500
+ /**
501
+ * Detach from the FFI and release resources: on `eos`, on `cancel()`, or
502
+ * because the track went away (e.g. it was unsubscribed — which never
503
+ * produces an `eos`, so without this the stream would keep delivering
504
+ * frames). Already-buffered frames stay readable and the consumer sees `done`
505
+ * after draining them.
506
+ *
507
+ * @remarks
508
+ * Idempotent, so `eos` arriving after a `cancel()` (or vice versa) doesn't
509
+ * double-dispose the handle while buffered frames are still queued.
510
+ *
511
+ * @internal
512
+ */
513
+ teardown(): void;
489
514
  cancel(): void;
490
515
  }
491
516
  declare class AudioStream extends ReadableStream<AudioFrame> {
@@ -53,16 +53,7 @@ class AudioStreamSource {
53
53
  this.controller.enqueue(frame);
54
54
  break;
55
55
  case "eos":
56
- import_ffi_client.FfiClient.instance.off(import_ffi_client.FfiClientEvent.FfiEvent, this.onEvent);
57
- this.controller.close();
58
- if (!this.disposed) {
59
- this.disposed = true;
60
- this.track.unregisterAudioStream(this);
61
- this.ffiHandle.dispose();
62
- if (this.frameProcessor && this.autoCloseProcessor) {
63
- this.frameProcessor.close();
64
- }
65
- }
56
+ this.teardown();
66
57
  break;
67
58
  }
68
59
  };
@@ -109,16 +100,38 @@ class AudioStreamSource {
109
100
  start(controller) {
110
101
  this.controller = controller;
111
102
  }
112
- cancel() {
103
+ /**
104
+ * Detach from the FFI and release resources: on `eos`, on `cancel()`, or
105
+ * because the track went away (e.g. it was unsubscribed — which never
106
+ * produces an `eos`, so without this the stream would keep delivering
107
+ * frames). Already-buffered frames stay readable and the consumer sees `done`
108
+ * after draining them.
109
+ *
110
+ * @remarks
111
+ * Idempotent, so `eos` arriving after a `cancel()` (or vice versa) doesn't
112
+ * double-dispose the handle while buffered frames are still queued.
113
+ *
114
+ * @internal
115
+ */
116
+ teardown() {
117
+ var _a;
113
118
  import_ffi_client.FfiClient.instance.off(import_ffi_client.FfiClientEvent.FfiEvent, this.onEvent);
114
- if (!this.disposed) {
115
- this.disposed = true;
116
- this.track.unregisterAudioStream(this);
117
- this.ffiHandle.dispose();
118
- if (this.frameProcessor && this.autoCloseProcessor) {
119
- this.frameProcessor.close();
120
- }
119
+ if (this.disposed) {
120
+ return;
121
121
  }
122
+ this.disposed = true;
123
+ this.track.unregisterAudioStream(this);
124
+ this.ffiHandle.dispose();
125
+ if (this.frameProcessor && this.autoCloseProcessor) {
126
+ this.frameProcessor.close();
127
+ }
128
+ try {
129
+ (_a = this.controller) == null ? void 0 : _a.close();
130
+ } catch {
131
+ }
132
+ }
133
+ cancel() {
134
+ this.teardown();
122
135
  }
123
136
  }
124
137
  class AudioStream extends ReadableStream {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { NewAudioStreamResponse } from '@livekit/rtc-ffi-bindings';\nimport { AudioStreamType, NewAudioStreamRequest } from '@livekit/rtc-ffi-bindings';\nimport type { UnderlyingSource } from 'node:stream/web';\nimport { AudioFrame } from './audio_frame.js';\nimport type { FfiEvent } from './ffi_client.js';\nimport { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js';\nimport { type FrameProcessor, isFrameProcessor } from './frame_processor.js';\nimport { log } from './log.js';\nimport type { Track } from './track.js';\n\nexport interface AudioStreamOptions {\n noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;\n /**\n * When the audio stream closes, whether to run the {@link FrameProcessor}'s\n * `close()` method. If `false`, the processor is left open so it can be\n * reused with another {@link AudioStream}. Only relevant when\n * `noiseCancellation` is a {@link FrameProcessor}. Defaults to `true`.\n */\n autoCloseNoiseCancellation?: boolean;\n sampleRate?: number;\n numChannels?: number;\n frameSizeMs?: number;\n}\n\nexport interface NoiseCancellationOptions {\n moduleId: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options: Record<string, any>;\n}\n\n/** @internal */\nexport class AudioStreamSource implements UnderlyingSource<AudioFrame> {\n private controller?: ReadableStreamDefaultController<AudioFrame>;\n private ffiHandle: FfiHandle;\n private disposed = false;\n private sampleRate: number;\n private numChannels: number;\n private legacyNcOptions?: NoiseCancellationOptions;\n private frameProcessor: FrameProcessor<AudioFrame> | null = null;\n private autoCloseProcessor = true;\n private frameSizeMs?: number;\n private track: Track;\n\n constructor(\n track: Track,\n sampleRateOrOptions?: number | AudioStreamOptions,\n numChannels?: number,\n ) {\n this.track = track;\n if (sampleRateOrOptions !== undefined && typeof sampleRateOrOptions !== 'number') {\n this.sampleRate = sampleRateOrOptions.sampleRate ?? 48000;\n this.numChannels = sampleRateOrOptions.numChannels ?? 1;\n if (isFrameProcessor(sampleRateOrOptions.noiseCancellation)) {\n this.frameProcessor = sampleRateOrOptions.noiseCancellation;\n this.autoCloseProcessor = sampleRateOrOptions.autoCloseNoiseCancellation ?? true;\n } else {\n this.legacyNcOptions = sampleRateOrOptions.noiseCancellation;\n }\n this.frameSizeMs = sampleRateOrOptions.frameSizeMs;\n } else {\n this.sampleRate = (sampleRateOrOptions as number) ?? 48000;\n this.numChannels = numChannels ?? 1;\n }\n\n const req = new NewAudioStreamRequest({\n type: AudioStreamType.AUDIO_STREAM_NATIVE,\n trackHandle: track.ffi_handle.handle,\n sampleRate: this.sampleRate,\n numChannels: this.numChannels,\n frameSizeMs: this.frameSizeMs,\n ...(this.legacyNcOptions\n ? {\n audioFilterModuleId: this.legacyNcOptions.moduleId,\n audioFilterOptions: JSON.stringify(this.legacyNcOptions.options),\n }\n : {}),\n });\n\n const res = FfiClient.instance.request<NewAudioStreamResponse>({\n message: {\n case: 'newAudioStream',\n value: req,\n },\n });\n\n this.ffiHandle = new FfiHandle(res.stream!.handle!.id!);\n\n FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent);\n track.registerAudioStream(this);\n }\n\n /** @internal */\n get processor(): FrameProcessor<AudioFrame> | null {\n return this.frameProcessor;\n }\n\n private onEvent = (ev: FfiEvent) => {\n if (!this.controller) {\n throw new Error('Stream controller not initialized');\n }\n\n if (\n ev.message.case != 'audioStreamEvent' ||\n ev.message.value.streamHandle != this.ffiHandle.handle\n ) {\n return;\n }\n\n const streamEvent = ev.message.value.message;\n switch (streamEvent.case) {\n case 'frameReceived':\n let frame = AudioFrame.fromOwnedInfo(streamEvent.value.frame!);\n if (this.frameProcessor && this.frameProcessor.isEnabled()) {\n try {\n frame = this.frameProcessor.process(frame);\n } catch (err: unknown) {\n log.warn(`Frame processing failed, passing through original frame: ${err}`);\n }\n }\n this.controller.enqueue(frame);\n break;\n case 'eos':\n FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);\n this.controller.close();\n // Dispose the native handle so the FD is released on stream end,\n // not just when cancel() is called explicitly by the consumer.\n // Guard against double-dispose if cancel() is called after EOS\n // while buffered frames are still in the ReadableStream queue.\n if (!this.disposed) {\n this.disposed = true;\n this.track.unregisterAudioStream(this);\n this.ffiHandle.dispose();\n if (this.frameProcessor && this.autoCloseProcessor) {\n this.frameProcessor.close();\n }\n }\n break;\n }\n };\n\n start(controller: ReadableStreamDefaultController<AudioFrame>) {\n this.controller = controller;\n }\n\n cancel() {\n FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);\n if (!this.disposed) {\n this.disposed = true;\n this.track.unregisterAudioStream(this);\n this.ffiHandle.dispose();\n // Also close the frame processor on cancel for symmetry with the EOS path,\n // so resources are released regardless of how the stream ends.\n if (this.frameProcessor && this.autoCloseProcessor) {\n this.frameProcessor.close();\n }\n }\n }\n}\n\nexport class AudioStream extends ReadableStream<AudioFrame> {\n constructor(track: Track);\n constructor(track: Track, sampleRate: number);\n constructor(track: Track, sampleRate: number, numChannels: number);\n constructor(track: Track, options: AudioStreamOptions);\n constructor(\n track: Track,\n sampleRateOrOptions?: number | AudioStreamOptions,\n numChannels?: number,\n ) {\n super(new AudioStreamSource(track, sampleRateOrOptions, numChannels));\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/audio_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { NewAudioStreamResponse } from '@livekit/rtc-ffi-bindings';\nimport { AudioStreamType, NewAudioStreamRequest } from '@livekit/rtc-ffi-bindings';\nimport type { UnderlyingSource } from 'node:stream/web';\nimport { AudioFrame } from './audio_frame.js';\nimport type { FfiEvent } from './ffi_client.js';\nimport { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js';\nimport { type FrameProcessor, isFrameProcessor } from './frame_processor.js';\nimport { log } from './log.js';\nimport type { Track } from './track.js';\n\nexport interface AudioStreamOptions {\n noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;\n /**\n * When the audio stream closes, whether to run the {@link FrameProcessor}'s\n * `close()` method. If `false`, the processor is left open so it can be\n * reused with another {@link AudioStream}. Only relevant when\n * `noiseCancellation` is a {@link FrameProcessor}. Defaults to `true`.\n */\n autoCloseNoiseCancellation?: boolean;\n sampleRate?: number;\n numChannels?: number;\n frameSizeMs?: number;\n}\n\nexport interface NoiseCancellationOptions {\n moduleId: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options: Record<string, any>;\n}\n\n/** @internal */\nexport class AudioStreamSource implements UnderlyingSource<AudioFrame> {\n private controller?: ReadableStreamDefaultController<AudioFrame>;\n private ffiHandle: FfiHandle;\n private disposed = false;\n private sampleRate: number;\n private numChannels: number;\n private legacyNcOptions?: NoiseCancellationOptions;\n private frameProcessor: FrameProcessor<AudioFrame> | null = null;\n private autoCloseProcessor = true;\n private frameSizeMs?: number;\n private track: Track;\n\n constructor(\n track: Track,\n sampleRateOrOptions?: number | AudioStreamOptions,\n numChannels?: number,\n ) {\n this.track = track;\n if (sampleRateOrOptions !== undefined && typeof sampleRateOrOptions !== 'number') {\n this.sampleRate = sampleRateOrOptions.sampleRate ?? 48000;\n this.numChannels = sampleRateOrOptions.numChannels ?? 1;\n if (isFrameProcessor(sampleRateOrOptions.noiseCancellation)) {\n this.frameProcessor = sampleRateOrOptions.noiseCancellation;\n this.autoCloseProcessor = sampleRateOrOptions.autoCloseNoiseCancellation ?? true;\n } else {\n this.legacyNcOptions = sampleRateOrOptions.noiseCancellation;\n }\n this.frameSizeMs = sampleRateOrOptions.frameSizeMs;\n } else {\n this.sampleRate = (sampleRateOrOptions as number) ?? 48000;\n this.numChannels = numChannels ?? 1;\n }\n\n const req = new NewAudioStreamRequest({\n type: AudioStreamType.AUDIO_STREAM_NATIVE,\n trackHandle: track.ffi_handle.handle,\n sampleRate: this.sampleRate,\n numChannels: this.numChannels,\n frameSizeMs: this.frameSizeMs,\n ...(this.legacyNcOptions\n ? {\n audioFilterModuleId: this.legacyNcOptions.moduleId,\n audioFilterOptions: JSON.stringify(this.legacyNcOptions.options),\n }\n : {}),\n });\n\n const res = FfiClient.instance.request<NewAudioStreamResponse>({\n message: {\n case: 'newAudioStream',\n value: req,\n },\n });\n\n this.ffiHandle = new FfiHandle(res.stream!.handle!.id!);\n\n FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent);\n track.registerAudioStream(this);\n }\n\n /** @internal */\n get processor(): FrameProcessor<AudioFrame> | null {\n return this.frameProcessor;\n }\n\n private onEvent = (ev: FfiEvent) => {\n if (!this.controller) {\n throw new Error('Stream controller not initialized');\n }\n\n if (\n ev.message.case != 'audioStreamEvent' ||\n ev.message.value.streamHandle != this.ffiHandle.handle\n ) {\n return;\n }\n\n const streamEvent = ev.message.value.message;\n switch (streamEvent.case) {\n case 'frameReceived':\n let frame = AudioFrame.fromOwnedInfo(streamEvent.value.frame!);\n if (this.frameProcessor && this.frameProcessor.isEnabled()) {\n try {\n frame = this.frameProcessor.process(frame);\n } catch (err: unknown) {\n log.warn(`Frame processing failed, passing through original frame: ${err}`);\n }\n }\n this.controller.enqueue(frame);\n break;\n case 'eos':\n // Disposes the native handle so the FD is released on stream end, not\n // just when cancel() is called explicitly by the consumer.\n this.teardown();\n break;\n }\n };\n\n start(controller: ReadableStreamDefaultController<AudioFrame>) {\n this.controller = controller;\n }\n\n /**\n * Detach from the FFI and release resources: on `eos`, on `cancel()`, or\n * because the track went away (e.g. it was unsubscribed — which never\n * produces an `eos`, so without this the stream would keep delivering\n * frames). Already-buffered frames stay readable and the consumer sees `done`\n * after draining them.\n *\n * @remarks\n * Idempotent, so `eos` arriving after a `cancel()` (or vice versa) doesn't\n * double-dispose the handle while buffered frames are still queued.\n *\n * @internal\n */\n teardown() {\n FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);\n if (this.disposed) {\n return;\n }\n this.disposed = true;\n this.track.unregisterAudioStream(this);\n this.ffiHandle.dispose();\n // Close the frame processor on every teardown path so resources are\n // released regardless of how the stream ended.\n if (this.frameProcessor && this.autoCloseProcessor) {\n this.frameProcessor.close();\n }\n try {\n this.controller?.close();\n } catch {\n // Already closed — e.g. cancel(), where the consumer has torn the\n // ReadableStream down before the underlying source is notified.\n }\n }\n\n cancel() {\n this.teardown();\n }\n}\n\nexport class AudioStream extends ReadableStream<AudioFrame> {\n constructor(track: Track);\n constructor(track: Track, sampleRate: number);\n constructor(track: Track, sampleRate: number, numChannels: number);\n constructor(track: Track, options: AudioStreamOptions);\n constructor(\n track: Track,\n sampleRateOrOptions?: number | AudioStreamOptions,\n numChannels?: number,\n ) {\n super(new AudioStreamSource(track, sampleRateOrOptions, numChannels));\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import 'node:stream/web';
2
2
  import './audio_frame.cjs';
3
3
  import './frame_processor.cjs';
4
- export { A as AudioStream, r as AudioStreamOptions, s as AudioStreamSource, N as NoiseCancellationOptions } from './audio_stream-B4hGVcKz.cjs';
4
+ export { A as AudioStream, r as AudioStreamOptions, s as AudioStreamSource, N as NoiseCancellationOptions } from './audio_stream-4M8XlO4-.cjs';
5
5
  import '@livekit/rtc-ffi-bindings';
6
6
  import './video_frame.cjs';
7
7
  import './audio_source.cjs';
@@ -1,7 +1,7 @@
1
1
  import 'node:stream/web';
2
2
  import './audio_frame.js';
3
3
  import './frame_processor.js';
4
- export { A as AudioStream, r as AudioStreamOptions, s as AudioStreamSource, N as NoiseCancellationOptions } from './audio_stream-DEG1JKge.js';
4
+ export { A as AudioStream, r as AudioStreamOptions, s as AudioStreamSource, N as NoiseCancellationOptions } from './audio_stream-CTyN1Ldy.js';
5
5
  import '@livekit/rtc-ffi-bindings';
6
6
  import './video_frame.js';
7
7
  import './audio_source.js';
@@ -1 +1 @@
1
- {"version":3,"file":"audio_stream.d.ts","sourceRoot":"","sources":["../src/audio_stream.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,sBAAsB,CAAC;AAE7E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC1E;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,gBAAgB;AAChB,qBAAa,iBAAkB,YAAW,gBAAgB,CAAC,UAAU,CAAC;IACpE,OAAO,CAAC,UAAU,CAAC,CAA8C;IACjE,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAC,CAA2B;IACnD,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,KAAK,CAAQ;gBAGnB,KAAK,EAAE,KAAK,EACZ,mBAAmB,CAAC,EAAE,MAAM,GAAG,kBAAkB,EACjD,WAAW,CAAC,EAAE,MAAM;IA6CtB,gBAAgB;IAChB,IAAI,SAAS,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAEjD;IAED,OAAO,CAAC,OAAO,CA0Cb;IAEF,KAAK,CAAC,UAAU,EAAE,+BAA+B,CAAC,UAAU,CAAC;IAI7D,MAAM;CAaP;AAED,qBAAa,WAAY,SAAQ,cAAc,CAAC,UAAU,CAAC;gBAC7C,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM;gBAChC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;gBACrD,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB;CAQtD"}
1
+ {"version":3,"file":"audio_stream.d.ts","sourceRoot":"","sources":["../src/audio_stream.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,sBAAsB,CAAC;AAE7E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC1E;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,gBAAgB;AAChB,qBAAa,iBAAkB,YAAW,gBAAgB,CAAC,UAAU,CAAC;IACpE,OAAO,CAAC,UAAU,CAAC,CAA8C;IACjE,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAC,CAA2B;IACnD,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,KAAK,CAAQ;gBAGnB,KAAK,EAAE,KAAK,EACZ,mBAAmB,CAAC,EAAE,MAAM,GAAG,kBAAkB,EACjD,WAAW,CAAC,EAAE,MAAM;IA6CtB,gBAAgB;IAChB,IAAI,SAAS,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAEjD;IAED,OAAO,CAAC,OAAO,CA+Bb;IAEF,KAAK,CAAC,UAAU,EAAE,+BAA+B,CAAC,UAAU,CAAC;IAI7D;;;;;;;;;;;;OAYG;IACH,QAAQ;IAqBR,MAAM;CAGP;AAED,qBAAa,WAAY,SAAQ,cAAc,CAAC,UAAU,CAAC;gBAC7C,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM;gBAChC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;gBACrD,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB;CAQtD"}
@@ -29,16 +29,7 @@ class AudioStreamSource {
29
29
  this.controller.enqueue(frame);
30
30
  break;
31
31
  case "eos":
32
- FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
33
- this.controller.close();
34
- if (!this.disposed) {
35
- this.disposed = true;
36
- this.track.unregisterAudioStream(this);
37
- this.ffiHandle.dispose();
38
- if (this.frameProcessor && this.autoCloseProcessor) {
39
- this.frameProcessor.close();
40
- }
41
- }
32
+ this.teardown();
42
33
  break;
43
34
  }
44
35
  };
@@ -85,16 +76,38 @@ class AudioStreamSource {
85
76
  start(controller) {
86
77
  this.controller = controller;
87
78
  }
88
- cancel() {
79
+ /**
80
+ * Detach from the FFI and release resources: on `eos`, on `cancel()`, or
81
+ * because the track went away (e.g. it was unsubscribed — which never
82
+ * produces an `eos`, so without this the stream would keep delivering
83
+ * frames). Already-buffered frames stay readable and the consumer sees `done`
84
+ * after draining them.
85
+ *
86
+ * @remarks
87
+ * Idempotent, so `eos` arriving after a `cancel()` (or vice versa) doesn't
88
+ * double-dispose the handle while buffered frames are still queued.
89
+ *
90
+ * @internal
91
+ */
92
+ teardown() {
93
+ var _a;
89
94
  FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
90
- if (!this.disposed) {
91
- this.disposed = true;
92
- this.track.unregisterAudioStream(this);
93
- this.ffiHandle.dispose();
94
- if (this.frameProcessor && this.autoCloseProcessor) {
95
- this.frameProcessor.close();
96
- }
95
+ if (this.disposed) {
96
+ return;
97
97
  }
98
+ this.disposed = true;
99
+ this.track.unregisterAudioStream(this);
100
+ this.ffiHandle.dispose();
101
+ if (this.frameProcessor && this.autoCloseProcessor) {
102
+ this.frameProcessor.close();
103
+ }
104
+ try {
105
+ (_a = this.controller) == null ? void 0 : _a.close();
106
+ } catch {
107
+ }
108
+ }
109
+ cancel() {
110
+ this.teardown();
98
111
  }
99
112
  }
100
113
  class AudioStream extends ReadableStream {