speechflow 2.2.1 → 2.3.1
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/{etc/claude.md → AGENTS.md} +8 -3
- package/CHANGELOG.md +98 -1
- package/README.md +28 -4
- package/etc/speechflow.yaml +3 -1
- package/etc/stx.conf +1 -1
- package/package.json +6 -6
- package/speechflow-cli/dst/speechflow-main-api.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-main-api.js +57 -16
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
- package/speechflow-cli/dst/speechflow-main-config.js +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.js +55 -21
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-nodes.js +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +6 -3
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +17 -19
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +25 -8
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +16 -13
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +7 -7
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +7 -4
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +21 -16
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +33 -11
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +42 -23
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +8 -2
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +24 -10
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +22 -7
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +10 -2
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +3 -0
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +160 -57
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +27 -15
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +84 -63
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +75 -20
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.js +5 -7
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +28 -15
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +2 -2
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.js +13 -3
- package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
- package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +9 -17
- package/speechflow-cli/dst/speechflow-util-queue.js +98 -78
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.js +1 -1
- package/speechflow-cli/dst/speechflow.d.ts +1 -1
- package/speechflow-cli/dst/speechflow.js +1 -1
- package/speechflow-cli/etc/eslint.mjs +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/etc/stx.conf +8 -2
- package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
- package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
- package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
- package/speechflow-cli/package.json +40 -30
- package/speechflow-cli/src/lib.d.ts +19 -1
- package/speechflow-cli/src/speechflow-main-api.ts +64 -19
- package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
- package/speechflow-cli/src/speechflow-main-config.ts +1 -1
- package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
- package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
- package/speechflow-cli/src/speechflow-main-status.ts +6 -3
- package/speechflow-cli/src/speechflow-main.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +19 -20
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +31 -13
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +17 -13
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +9 -8
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +8 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +22 -18
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +47 -25
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +12 -4
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +24 -10
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +22 -7
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +10 -2
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +215 -62
- package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
- package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +30 -16
- package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +92 -70
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +79 -22
- package/speechflow-cli/src/speechflow-node.ts +7 -8
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
- package/speechflow-cli/src/speechflow-util-audio.ts +31 -17
- package/speechflow-cli/src/speechflow-util-error.ts +3 -3
- package/speechflow-cli/src/speechflow-util-llm.ts +14 -3
- package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
- package/speechflow-cli/src/speechflow-util-queue.ts +103 -81
- package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
- package/speechflow-cli/src/speechflow-util.ts +1 -1
- package/speechflow-cli/src/speechflow.ts +1 -1
- package/speechflow-ui-db/dst/index.html +1 -1
- package/speechflow-ui-db/dst/index.js +15 -15
- package/speechflow-ui-db/etc/eslint.mjs +1 -1
- package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-db/etc/stx.conf +1 -1
- package/speechflow-ui-db/etc/stylelint.js +1 -1
- package/speechflow-ui-db/etc/stylelint.yaml +1 -1
- package/speechflow-ui-db/etc/vite-client.mts +1 -1
- package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-db/package.json +22 -16
- package/speechflow-ui-db/src/app.styl +1 -1
- package/speechflow-ui-db/src/app.vue +1 -1
- package/speechflow-ui-db/src/index.html +1 -1
- package/speechflow-ui-db/src/index.ts +1 -1
- package/speechflow-ui-st/dst/index.html +1 -1
- package/speechflow-ui-st/dst/index.js +31 -31
- package/speechflow-ui-st/etc/eslint.mjs +1 -1
- package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-st/etc/stx.conf +1 -1
- package/speechflow-ui-st/etc/stylelint.js +1 -1
- package/speechflow-ui-st/etc/stylelint.yaml +1 -1
- package/speechflow-ui-st/etc/vite-client.mts +1 -1
- package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-st/package.json +23 -17
- package/speechflow-ui-st/src/app.styl +1 -1
- package/speechflow-ui-st/src/app.vue +1 -1
- package/speechflow-ui-st/src/index.html +1 -1
- package/speechflow-ui-st/src/index.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -39,13 +39,14 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
39
39
|
/* internal state */
|
|
40
40
|
private peerConnections = new Map<string, WebRTCConnection>()
|
|
41
41
|
private httpServer: http.Server | null = null
|
|
42
|
-
private chunkQueue: util.
|
|
42
|
+
private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
|
|
43
43
|
private opusEncoder: OpusEncoder | null = null
|
|
44
44
|
private opusDecoder: OpusEncoder | null = null
|
|
45
45
|
private pcmBuffer = Buffer.alloc(0)
|
|
46
46
|
private rtpSequence = 0
|
|
47
47
|
private rtpTimestamp = 0
|
|
48
48
|
private rtpSSRC = 0
|
|
49
|
+
private rtpMarkerNext = true
|
|
49
50
|
private maxConnections = 10
|
|
50
51
|
|
|
51
52
|
/* Opus codec configuration: 48kHz, mono, 16-bit */
|
|
@@ -177,7 +178,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
177
178
|
padding: false,
|
|
178
179
|
paddingSize: 0,
|
|
179
180
|
extension: false,
|
|
180
|
-
marker:
|
|
181
|
+
marker: this.rtpMarkerNext,
|
|
181
182
|
payloadType: 111, /* Opus payload type */
|
|
182
183
|
sequenceNumber: this.rtpSequence++ & 0xFFFF,
|
|
183
184
|
timestamp: this.rtpTimestamp,
|
|
@@ -186,6 +187,9 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
186
187
|
extensions: []
|
|
187
188
|
})
|
|
188
189
|
|
|
190
|
+
/* clear marker (set only on first packet of a talkspurt per RFC 3551) */
|
|
191
|
+
this.rtpMarkerNext = false
|
|
192
|
+
|
|
189
193
|
/* build RTP packet */
|
|
190
194
|
const rtpPacket = new RtpPacket(rtpHeader, opusPacket)
|
|
191
195
|
|
|
@@ -265,11 +269,11 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
265
269
|
const resourceId = crypto.randomUUID()
|
|
266
270
|
const { pc, subscription } = this.createPeerConnection(resourceId)
|
|
267
271
|
|
|
268
|
-
/* protocol-specific setup */
|
|
269
|
-
const track = setupFn(pc, resourceId)
|
|
270
|
-
|
|
271
272
|
/* complete SDP offer/answer exchange and establish connection */
|
|
272
273
|
try {
|
|
274
|
+
/* protocol-specific setup */
|
|
275
|
+
const track = setupFn(pc, resourceId)
|
|
276
|
+
|
|
273
277
|
/* set remote description (offer from client) */
|
|
274
278
|
await pc.setRemoteDescription({ type: "offer", sdp: offer })
|
|
275
279
|
|
|
@@ -365,9 +369,10 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
365
369
|
this.rtpSequence = Math.floor(Math.random() * 0x10000)
|
|
366
370
|
this.rtpTimestamp = Math.floor(Math.random() * 0x100000000) >>> 0
|
|
367
371
|
this.rtpSSRC = Math.floor(Math.random() * 0x100000000) >>> 0
|
|
372
|
+
this.rtpMarkerNext = true
|
|
368
373
|
|
|
369
374
|
/* setup chunk queue for incoming audio */
|
|
370
|
-
this.chunkQueue = new util.
|
|
375
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
371
376
|
|
|
372
377
|
/* parse listen address */
|
|
373
378
|
const listen = this.parseAddress(this.params.listen, 8085)
|
|
@@ -375,77 +380,89 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
375
380
|
/* setup HTTP server for WHIP/WHEP signaling */
|
|
376
381
|
const self = this
|
|
377
382
|
this.httpServer = http.createServer(async (req, res) => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/* handle requests... */
|
|
403
|
-
if (req.method === "POST" && pathMatch) {
|
|
404
|
-
/* handle WHIP/WHEP POST */
|
|
405
|
-
const body = await self.readRequestBody(req)
|
|
406
|
-
|
|
407
|
-
/* sanity check content type */
|
|
408
|
-
const contentType = req.headers["content-type"]
|
|
409
|
-
if (contentType !== "application/sdp") {
|
|
410
|
-
res.writeHead(415, { "Content-Type": "text/plain" })
|
|
411
|
-
res.end("Unsupported Media Type")
|
|
383
|
+
try {
|
|
384
|
+
/* determine URL */
|
|
385
|
+
if (req.url === undefined) {
|
|
386
|
+
res.writeHead(400, { "Content-Type": "text/plain" })
|
|
387
|
+
res.end("Bad Request")
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
const host = req.headers.host?.replace(/[^a-zA-Z0-9:.\-_]/g, "") ?? "localhost"
|
|
391
|
+
const url = new URL(req.url, `http://${host}`)
|
|
392
|
+
const pathMatch = url.pathname === self.params.path
|
|
393
|
+
const resourceMatch = url.pathname.startsWith(self.params.path + "/")
|
|
394
|
+
|
|
395
|
+
/* CORS headers for browser clients */
|
|
396
|
+
res.setHeader("Access-Control-Allow-Origin", "*")
|
|
397
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, OPTIONS")
|
|
398
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
|
399
|
+
res.setHeader("Access-Control-Expose-Headers", "Location")
|
|
400
|
+
|
|
401
|
+
/* handle CORS preflight */
|
|
402
|
+
if (req.method === "OPTIONS") {
|
|
403
|
+
res.writeHead(204)
|
|
404
|
+
res.end()
|
|
412
405
|
return
|
|
413
406
|
}
|
|
414
407
|
|
|
415
|
-
/*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
408
|
+
/* handle requests... */
|
|
409
|
+
if (req.method === "POST" && pathMatch) {
|
|
410
|
+
/* handle WHIP/WHEP POST */
|
|
411
|
+
const body = await self.readRequestBody(req)
|
|
412
|
+
|
|
413
|
+
/* sanity check content type */
|
|
414
|
+
const contentType = req.headers["content-type"]
|
|
415
|
+
if (contentType !== "application/sdp") {
|
|
416
|
+
res.writeHead(415, { "Content-Type": "text/plain" })
|
|
417
|
+
res.end("Unsupported Media Type")
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* determine if WHIP (receiving) or WHEP (sending) based on SDP content */
|
|
422
|
+
const hasSendonly = /\ba=sendonly\b/m.test(body)
|
|
423
|
+
const hasSendrecv = /\ba=sendrecv\b/m.test(body)
|
|
424
|
+
const hasRecvonly = /\ba=recvonly\b/m.test(body)
|
|
425
|
+
const isPublisher = hasSendonly || hasSendrecv
|
|
426
|
+
const isViewer = hasRecvonly
|
|
427
|
+
|
|
428
|
+
/* handle protocol based on mode */
|
|
429
|
+
if (self.params.mode === "r" && isPublisher)
|
|
430
|
+
/* in read mode, accept WHIP publishers */
|
|
431
|
+
await self.handleWHIP(res, body)
|
|
432
|
+
else if (self.params.mode === "w" && isViewer)
|
|
433
|
+
/* in write mode, accept WHEP viewers */
|
|
434
|
+
await self.handleWHEP(res, body)
|
|
435
|
+
else {
|
|
436
|
+
res.writeHead(403, { "Content-Type": "text/plain" })
|
|
437
|
+
res.end("Forbidden")
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (req.method === "DELETE" && resourceMatch) {
|
|
441
|
+
/* handle DELETE for connection teardown */
|
|
442
|
+
const resourceId = url.pathname.substring(self.params.path.length + 1)
|
|
443
|
+
self.handleDELETE(res, resourceId)
|
|
444
|
+
}
|
|
429
445
|
else {
|
|
430
|
-
|
|
431
|
-
res.
|
|
446
|
+
/* handle unknown requests */
|
|
447
|
+
res.writeHead(404, { "Content-Type": "text/plain" })
|
|
448
|
+
res.end("Not Found")
|
|
432
449
|
}
|
|
433
450
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
/* handle unknown requests */
|
|
441
|
-
res.writeHead(404, { "Content-Type": "text/plain" })
|
|
442
|
-
res.end("Not Found")
|
|
451
|
+
catch (err: unknown) {
|
|
452
|
+
self.log("error", `HTTP request handler failed: ${util.ensureError(err).message}`)
|
|
453
|
+
if (!res.headersSent) {
|
|
454
|
+
res.writeHead(500, { "Content-Type": "text/plain" })
|
|
455
|
+
res.end("Internal Server Error")
|
|
456
|
+
}
|
|
443
457
|
}
|
|
444
458
|
})
|
|
445
459
|
|
|
446
460
|
/* start HTTP server */
|
|
447
|
-
await new Promise<void>((resolve) => {
|
|
461
|
+
await new Promise<void>((resolve, reject) => {
|
|
462
|
+
const onError = (err: Error) => { reject(err) }
|
|
463
|
+
this.httpServer!.once("error", onError)
|
|
448
464
|
this.httpServer!.listen(listen.port, listen.host, () => {
|
|
465
|
+
this.httpServer!.removeListener("error", onError)
|
|
449
466
|
const mode = this.params.mode === "r" ? "WHIP" : "WHEP"
|
|
450
467
|
this.log("info", `WebRTC ${mode} server listening on http://${listen.host}:${listen.port}${this.params.path}`)
|
|
451
468
|
resolve()
|
|
@@ -470,6 +487,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
470
487
|
}
|
|
471
488
|
if (self.peerConnections.size === 0) {
|
|
472
489
|
/* silently drop if no viewers connected */
|
|
490
|
+
self.rtpMarkerNext = true
|
|
473
491
|
callback()
|
|
474
492
|
return
|
|
475
493
|
}
|
|
@@ -486,11 +504,15 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
486
504
|
this.push(null)
|
|
487
505
|
return
|
|
488
506
|
}
|
|
489
|
-
|
|
507
|
+
if (self.chunkQueue === null)
|
|
508
|
+
return
|
|
509
|
+
const queue = self.chunkQueue
|
|
510
|
+
reads.add(queue.read().then((chunk) => {
|
|
490
511
|
this.push(chunk, "binary")
|
|
491
512
|
}).catch((err: Error) => {
|
|
492
513
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
493
|
-
|
|
514
|
+
if (queue.destroyed)
|
|
515
|
+
this.push(null)
|
|
494
516
|
}))
|
|
495
517
|
}
|
|
496
518
|
})
|
|
@@ -517,7 +539,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
517
539
|
|
|
518
540
|
/* drain and clear chunk queue */
|
|
519
541
|
if (this.chunkQueue !== null) {
|
|
520
|
-
this.chunkQueue.
|
|
542
|
+
this.chunkQueue.destroy()
|
|
521
543
|
this.chunkQueue = null
|
|
522
544
|
}
|
|
523
545
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -21,8 +21,9 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
21
21
|
public static name = "xio-websocket"
|
|
22
22
|
|
|
23
23
|
/* internal state */
|
|
24
|
-
private server:
|
|
25
|
-
private client:
|
|
24
|
+
private server: ws.WebSocketServer | null = null
|
|
25
|
+
private client: ReconnWebSocket | null = null
|
|
26
|
+
private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
|
|
26
27
|
|
|
27
28
|
/* construct node */
|
|
28
29
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
@@ -63,7 +64,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
63
64
|
/* listen locally on a Websocket port */
|
|
64
65
|
const url = new URL(this.params.listen)
|
|
65
66
|
const websockets = new Set<ws.WebSocket>()
|
|
66
|
-
|
|
67
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
67
68
|
this.server = new ws.WebSocketServer({
|
|
68
69
|
host: url.hostname,
|
|
69
70
|
port: Number.parseInt(url.port, 10),
|
|
@@ -101,8 +102,13 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
101
102
|
buffer = Buffer.from(data)
|
|
102
103
|
else
|
|
103
104
|
buffer = Buffer.concat(data)
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
try {
|
|
106
|
+
const chunk = util.streamChunkDecode(buffer)
|
|
107
|
+
this.chunkQueue?.write(chunk)
|
|
108
|
+
}
|
|
109
|
+
catch (_err: unknown) {
|
|
110
|
+
this.log("warning", `received invalid CBOR chunk on URL ${this.params.listen} from peer ${peer}`)
|
|
111
|
+
}
|
|
106
112
|
})
|
|
107
113
|
})
|
|
108
114
|
this.server.on("error", (error) => {
|
|
@@ -124,9 +130,11 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
124
130
|
callback(new Error("still no WebSocket connections available"))
|
|
125
131
|
else {
|
|
126
132
|
const data = util.streamChunkEncode(chunk)
|
|
127
|
-
const
|
|
133
|
+
const sends: Promise<void>[] = []
|
|
134
|
+
const clients: ws.WebSocket[] = []
|
|
128
135
|
for (const websocket of websockets.values()) {
|
|
129
|
-
|
|
136
|
+
clients.push(websocket)
|
|
137
|
+
sends.push(new Promise<void>((resolve, reject) => {
|
|
130
138
|
websocket.send(data, (error) => {
|
|
131
139
|
if (error)
|
|
132
140
|
reject(error)
|
|
@@ -135,10 +143,27 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
135
143
|
})
|
|
136
144
|
}))
|
|
137
145
|
}
|
|
138
|
-
Promise.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
Promise.allSettled(sends).then((results) => {
|
|
147
|
+
let lastError: Error | null = null
|
|
148
|
+
for (let i = 0; i < results.length; i++) {
|
|
149
|
+
if (results[i].status === "rejected") {
|
|
150
|
+
const error = util.ensureError((results[i] as PromiseRejectedResult).reason)
|
|
151
|
+
self.log("warning", `failed to send to WebSocket client: ${error.message}`)
|
|
152
|
+
websockets.delete(clients[i])
|
|
153
|
+
clients[i].terminate()
|
|
154
|
+
lastError = error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const failures = results.filter((r) => r.status === "rejected").length
|
|
158
|
+
if (failures > 0 && failures < results.length)
|
|
159
|
+
self.log("warning", "partial broadcast failure: " +
|
|
160
|
+
`${failures} of ${results.length} WebSocket clients failed`)
|
|
161
|
+
if (lastError !== null && failures === results.length)
|
|
162
|
+
callback(lastError)
|
|
163
|
+
else
|
|
164
|
+
callback()
|
|
165
|
+
}).catch((err: Error) => {
|
|
166
|
+
callback(err)
|
|
142
167
|
})
|
|
143
168
|
}
|
|
144
169
|
},
|
|
@@ -147,12 +172,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
147
172
|
callback()
|
|
148
173
|
},
|
|
149
174
|
read (size: number) {
|
|
150
|
-
if (self.params.mode === "w")
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
if (self.params.mode === "w") {
|
|
176
|
+
self.log("error", "read operation on write-only node")
|
|
177
|
+
this.push(null)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (self.chunkQueue === null)
|
|
181
|
+
return
|
|
182
|
+
const queue = self.chunkQueue
|
|
183
|
+
reads.add(queue.read().then((chunk) => {
|
|
153
184
|
this.push(chunk, "binary")
|
|
154
185
|
}).catch((err: Error) => {
|
|
155
186
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
187
|
+
if (queue.destroyed)
|
|
188
|
+
this.push(null)
|
|
156
189
|
}))
|
|
157
190
|
}
|
|
158
191
|
})
|
|
@@ -178,7 +211,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
178
211
|
const error = util.ensureError(ev.error)
|
|
179
212
|
this.log("error", `error of connection on URL ${this.params.connect}: ${error.message}`)
|
|
180
213
|
})
|
|
181
|
-
|
|
214
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
182
215
|
this.client.addEventListener("message", (ev: MessageEvent) => {
|
|
183
216
|
if (this.params.mode === "w") {
|
|
184
217
|
this.log("warning", `connection to URL ${this.params.connect}: ` +
|
|
@@ -191,8 +224,13 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
191
224
|
return
|
|
192
225
|
}
|
|
193
226
|
const buffer = Buffer.from(ev.data)
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
try {
|
|
228
|
+
const chunk = util.streamChunkDecode(buffer)
|
|
229
|
+
this.chunkQueue?.write(chunk)
|
|
230
|
+
}
|
|
231
|
+
catch (_err: unknown) {
|
|
232
|
+
this.log("warning", `received invalid CBOR chunk from URL ${this.params.connect}`)
|
|
233
|
+
}
|
|
196
234
|
})
|
|
197
235
|
this.client.binaryType = "arraybuffer"
|
|
198
236
|
const self = this
|
|
@@ -207,7 +245,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
207
245
|
callback(new Error("write operation on read-only node"))
|
|
208
246
|
else if (chunk.type !== self.params.type)
|
|
209
247
|
callback(new Error(`written chunk is not of ${self.params.type} type`))
|
|
210
|
-
else if (
|
|
248
|
+
else if (self.client!.readyState !== ReconnWebSocket.OPEN)
|
|
211
249
|
callback(new Error("still no WebSocket connection available"))
|
|
212
250
|
else {
|
|
213
251
|
const data = util.streamChunkEncode(chunk)
|
|
@@ -220,12 +258,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
220
258
|
callback()
|
|
221
259
|
},
|
|
222
260
|
read (size: number) {
|
|
223
|
-
if (self.params.mode === "w")
|
|
224
|
-
|
|
225
|
-
|
|
261
|
+
if (self.params.mode === "w") {
|
|
262
|
+
self.log("error", "read operation on write-only node")
|
|
263
|
+
this.push(null)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
if (self.chunkQueue === null)
|
|
267
|
+
return
|
|
268
|
+
const queue = self.chunkQueue
|
|
269
|
+
reads.add(queue.read().then((chunk) => {
|
|
226
270
|
this.push(chunk, "binary")
|
|
227
271
|
}).catch((err: Error) => {
|
|
228
272
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
273
|
+
if (queue.destroyed)
|
|
274
|
+
this.push(null)
|
|
229
275
|
}))
|
|
230
276
|
}
|
|
231
277
|
})
|
|
@@ -234,8 +280,19 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
234
280
|
|
|
235
281
|
/* close node */
|
|
236
282
|
async close () {
|
|
283
|
+
/* drain and clear chunk queue reference */
|
|
284
|
+
if (this.chunkQueue !== null) {
|
|
285
|
+
this.chunkQueue.destroy()
|
|
286
|
+
this.chunkQueue = null
|
|
287
|
+
}
|
|
288
|
+
|
|
237
289
|
/* close WebSocket server */
|
|
238
290
|
if (this.server !== null) {
|
|
291
|
+
/* forcibly terminate all active client connections */
|
|
292
|
+
for (const client of this.server.clients)
|
|
293
|
+
client.terminate()
|
|
294
|
+
|
|
295
|
+
/* close connection */
|
|
239
296
|
await new Promise<void>((resolve, reject) => {
|
|
240
297
|
this.server!.close((error) => {
|
|
241
298
|
if (error) reject(error)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,7 +9,10 @@ import Events, { EventEmitter } from "node:events"
|
|
|
9
9
|
import Stream from "node:stream"
|
|
10
10
|
|
|
11
11
|
/* external dependencies */
|
|
12
|
-
import { DateTime, Duration }
|
|
12
|
+
import { DateTime, Duration } from "luxon"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import { deepClone } from "./speechflow-util-misc"
|
|
13
16
|
|
|
14
17
|
/* the definition of a single payload chunk passed through the SpeechFlow nodes */
|
|
15
18
|
export class SpeechFlowChunk {
|
|
@@ -33,7 +36,7 @@ export class SpeechFlowChunk {
|
|
|
33
36
|
this.kind,
|
|
34
37
|
this.type,
|
|
35
38
|
payload,
|
|
36
|
-
|
|
39
|
+
deepClone(this.meta)
|
|
37
40
|
)
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -140,16 +143,12 @@ export default class SpeechFlowNode extends Events.EventEmitter {
|
|
|
140
143
|
if (typeof this.args[spec[name].pos] !== spec[name].type)
|
|
141
144
|
throw new Error(`invalid type of positional parameter "${name}" ` +
|
|
142
145
|
`(has to be ${spec[name].type})`)
|
|
143
|
-
if ("match" in spec[name]
|
|
144
|
-
&& this.args[spec[name].pos].match(spec[name].match) === null)
|
|
145
|
-
throw new Error(`invalid value of positional parameter "${name}" ` +
|
|
146
|
-
`(has to match ${spec[name].match})`)
|
|
147
146
|
if ("match" in spec[name]
|
|
148
147
|
&& ( ( spec[name].match instanceof RegExp
|
|
149
148
|
&& this.args[spec[name].pos].match(spec[name].match) === null)
|
|
150
149
|
|| ( typeof spec[name].match === "function"
|
|
151
150
|
&& !spec[name].match(this.args[spec[name].pos]) ) ))
|
|
152
|
-
throw new Error(`invalid value "${this.
|
|
151
|
+
throw new Error(`invalid value "${this.args[spec[name].pos!]}" of positional parameter "${name}"`)
|
|
153
152
|
this.params[name] = this.args[spec[name].pos]
|
|
154
153
|
}
|
|
155
154
|
else if ("val" in spec[name] && spec[name].val !== undefined)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -15,7 +15,14 @@ interface StartCaptureMessage {
|
|
|
15
15
|
chunkId: string
|
|
16
16
|
expectedSamples: number
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
interface CancelCaptureMessage {
|
|
19
|
+
type: "cancel-capture"
|
|
20
|
+
chunkId: string
|
|
21
|
+
}
|
|
22
|
+
interface CancelAllCapturesMessage {
|
|
23
|
+
type: "cancel-all-captures"
|
|
24
|
+
}
|
|
25
|
+
type WorkletMessage = InputChunkMessage | StartCaptureMessage | CancelCaptureMessage | CancelAllCapturesMessage
|
|
19
26
|
interface ChunkData {
|
|
20
27
|
data: Float32Array
|
|
21
28
|
chunkId: string
|
|
@@ -24,6 +31,10 @@ interface ChunkStartedMessage {
|
|
|
24
31
|
type: "chunk-started"
|
|
25
32
|
chunkId: string
|
|
26
33
|
}
|
|
34
|
+
interface CaptureReadyMessage {
|
|
35
|
+
type: "capture-ready"
|
|
36
|
+
chunkId: string
|
|
37
|
+
}
|
|
27
38
|
interface CaptureCompleteMessage {
|
|
28
39
|
type: "capture-complete"
|
|
29
40
|
chunkId: string
|
|
@@ -43,9 +54,12 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
|
|
|
43
54
|
|
|
44
55
|
/* receive input chunks */
|
|
45
56
|
this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
|
|
46
|
-
const { type
|
|
57
|
+
const { type } = event.data
|
|
47
58
|
if (type === "input-chunk")
|
|
48
|
-
this.pendingData.push({
|
|
59
|
+
this.pendingData.push({
|
|
60
|
+
data: event.data.data.pcmData,
|
|
61
|
+
chunkId: event.data.chunkId
|
|
62
|
+
})
|
|
49
63
|
})
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -114,7 +128,8 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
|
|
|
114
128
|
/* audio capture node */
|
|
115
129
|
class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
116
130
|
/* internal state */
|
|
117
|
-
private
|
|
131
|
+
private static readonly CAPTURE_TTL = 30 * 1000
|
|
132
|
+
private activeCaptures = new Map<string, { data: number[], expectedSamples: number, createdAt: number }>()
|
|
118
133
|
|
|
119
134
|
/* node construction */
|
|
120
135
|
constructor () {
|
|
@@ -122,13 +137,28 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
|
122
137
|
|
|
123
138
|
/* receive start of capturing command */
|
|
124
139
|
this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
|
|
125
|
-
const { type
|
|
140
|
+
const { type } = event.data
|
|
126
141
|
if (type === "start-capture") {
|
|
142
|
+
const chunkId = event.data.chunkId
|
|
127
143
|
this.activeCaptures.set(chunkId, {
|
|
128
144
|
data: [],
|
|
129
|
-
expectedSamples: event.data.expectedSamples
|
|
145
|
+
expectedSamples: event.data.expectedSamples,
|
|
146
|
+
createdAt: Date.now()
|
|
130
147
|
})
|
|
148
|
+
|
|
149
|
+
/* acknowledge capture registration */
|
|
150
|
+
const ready: CaptureReadyMessage = {
|
|
151
|
+
type: "capture-ready",
|
|
152
|
+
chunkId
|
|
153
|
+
}
|
|
154
|
+
this.port.postMessage(ready)
|
|
131
155
|
}
|
|
156
|
+
else if (type === "cancel-capture") {
|
|
157
|
+
const chunkId = event.data.chunkId
|
|
158
|
+
this.activeCaptures.delete(chunkId)
|
|
159
|
+
}
|
|
160
|
+
else if (type === "cancel-all-captures")
|
|
161
|
+
this.activeCaptures.clear()
|
|
132
162
|
})
|
|
133
163
|
}
|
|
134
164
|
|
|
@@ -145,6 +175,15 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
|
145
175
|
const frameCount = input[0].length
|
|
146
176
|
const channelCount = input.length
|
|
147
177
|
|
|
178
|
+
/* evict stale captures (TTL safety net) */
|
|
179
|
+
const currentTime = Date.now()
|
|
180
|
+
for (const [ chunkId, capture ] of this.activeCaptures) {
|
|
181
|
+
if ((currentTime - capture.createdAt) > AudioCaptureProcessor.CAPTURE_TTL)
|
|
182
|
+
this.activeCaptures.delete(chunkId)
|
|
183
|
+
}
|
|
184
|
+
if (this.activeCaptures.size === 0)
|
|
185
|
+
return true
|
|
186
|
+
|
|
148
187
|
/* iterate over all active captures */
|
|
149
188
|
for (const [ chunkId, capture ] of this.activeCaptures) {
|
|
150
189
|
/* convert planar to interleaved */
|