@livekit/rtc-node 0.13.30 → 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.
- package/dist/audio_source.cjs +38 -19
- package/dist/audio_source.cjs.map +1 -1
- package/dist/audio_source.d.cts +12 -4
- package/dist/audio_source.d.ts +12 -4
- package/dist/audio_source.d.ts.map +1 -1
- package/dist/audio_source.js +38 -19
- package/dist/audio_source.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
- package/src/audio_source.test.ts +92 -0
- package/src/audio_source.ts +42 -18
- package/src/version.ts +1 -1
package/dist/audio_source.cjs
CHANGED
|
@@ -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.
|
|
30
|
-
|
|
31
|
-
this.
|
|
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.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,8BAMO;AAEP,wBAA0B;AAEnB,MAAM,YAAY;AAAA,EAsBvB,YAAY,YAAoB,aAAqB,YAAY,KAAM;AAZvE;AAAA,mBAA0B;AAE1B;AAAA,0BAA8B;AAE9B;AAAA,mBAA0C;AAE1C;AAAA,kBAAS;AA8DT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAgB,MAAM;AAlGxB;AAmGI,UAAI,CAAC,KAAK,SAAS;AACjB;AAAA,MACF;AAEA,iBAAK,mBAAL;AACA,WAAK,cAAc;AACnB,WAAK,mBAAmB;AACxB,WAAK,UAAU;AACf,WAAK,iBAAiB;AAGtB,UAAI,KAAK,SAAS;AAChB,qBAAa,KAAK,OAAO;AACzB,aAAK,UAAU;AAAA,MACjB;AAAA,IACF;AAvEE,SAAK,aAAa;AAClB,SAAK,cAAc;AACnB,SAAK,YAAY;AAEjB,SAAK,cAAc;AACnB,SAAK,mBAAmB;AAExB,UAAM,MAAM,IAAI,8CAAsB;AAAA,MACpC,MAAM,wCAAgB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,aAAa;AAAA,IACf,CAAC;AAED,UAAM,MAAM,4BAAU,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,kCAAU,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,gDAAwB;AAAA,MACtC,cAAc,KAAK,UAAU;AAAA,IAC/B,CAAC;AAED,gCAAU,SAAS,QAAkC;AAAA,MACnD,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,SAAK,cAAc;AAAA,EACrB;AAAA,EA4BA,MAAM,iBAAiB;AACrB,QAAI,CAAC,KAAK,SAAS;AACjB;AAAA,IACF;AAEA,UAAM,KAAK;AAAA,EACb;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,QAAI,CAAC,KAAK,SAAS;AACjB,WAAK,UAAU,IAAI,QAAc,CAAC,YAAY;AAC5C,aAAK,iBAAiB;AAAA,MACxB,CAAC;AAAA,IACH;AAEA,SAAK,UAAU,WAAW,KAAK,eAAe,KAAK,gBAAgB;AAEnE,UAAM,MAAM,IAAI,iDAAyB;AAAA,MACvC,cAAc,KAAK,UAAU;AAAA,MAC7B,QAAQ,MAAM,UAAU;AAAA,IAC1B,CAAC;AAED,UAAM,MAAM,4BAAU,SAAS,QAAmC;AAAA,MAChE,SAAS,EAAE,MAAM,qBAAqB,OAAO,IAAI;AAAA,IACnD,CAAC;AAED,UAAM,KAAK,MAAM,4BAAU,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,cAAc;AACnB,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS;AAAA,EAChB;AACF;","names":[]}
|
package/dist/audio_source.d.cts
CHANGED
|
@@ -11,8 +11,9 @@ declare class AudioSource {
|
|
|
11
11
|
/** @internal */
|
|
12
12
|
currentQueueSize: number;
|
|
13
13
|
/** @internal */
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/**
|
|
27
|
-
|
|
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>;
|
package/dist/audio_source.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ declare class AudioSource {
|
|
|
11
11
|
/** @internal */
|
|
12
12
|
currentQueueSize: number;
|
|
13
13
|
/** @internal */
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/**
|
|
27
|
-
|
|
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,
|
|
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"}
|
package/dist/audio_source.js
CHANGED
|
@@ -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.
|
|
13
|
-
|
|
14
|
-
this.
|
|
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.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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.
|
|
126
|
+
this.releaseWaiter();
|
|
108
127
|
this.ffiHandle.dispose();
|
|
109
128
|
this.closed = true;
|
|
110
129
|
}
|
package/dist/audio_source.js.map
CHANGED
|
@@ -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
|
|
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":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.13.
|
|
24
|
+
const SDK_VERSION = "0.13.31";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.31\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.31\";\n"],"mappings":"AAAO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LiveKit RTC Node",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "LiveKit",
|
|
6
|
-
"version": "0.13.
|
|
6
|
+
"version": "0.13.31",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"require": "dist/index.cjs",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"tsup": "^8.3.5",
|
|
45
45
|
"typescript": "5.8.2",
|
|
46
46
|
"vitest": "^4.0.0",
|
|
47
|
-
"livekit-server-sdk": "2.
|
|
47
|
+
"livekit-server-sdk": "2.17.0"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">= 18"
|
|
@@ -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
|
+
});
|
package/src/audio_source.ts
CHANGED
|
@@ -28,8 +28,9 @@ export class AudioSource {
|
|
|
28
28
|
/** @internal */
|
|
29
29
|
currentQueueSize: number;
|
|
30
30
|
/** @internal */
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
88
|
-
this.release();
|
|
88
|
+
this.releaseWaiter();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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.
|
|
179
|
+
this.releaseWaiter();
|
|
156
180
|
this.ffiHandle.dispose();
|
|
157
181
|
this.closed = true;
|
|
158
182
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.13.
|
|
1
|
+
export const SDK_VERSION = "0.13.31";
|