@qvac/decoder-audio 0.3.7 → 0.4.0
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/NOTICE +48 -32
- package/README.md +2 -2
- package/index.d.ts +5 -17
- package/index.js +112 -174
- package/package.json +5 -7
- package/utils/createStreamAccumulator.js +1 -1
- package/utils/error.js +21 -1
package/NOTICE
CHANGED
|
@@ -14,22 +14,29 @@ JavaScript Dependencies
|
|
|
14
14
|
|
|
15
15
|
@hyperswarm/secret-stream@6.9.1
|
|
16
16
|
https://github.com/holepunchto/hyperswarm-secret-stream
|
|
17
|
+
@qvac/diagnostics@0.1.1
|
|
17
18
|
@qvac/dl-base@0.1.1
|
|
18
19
|
@qvac/dl-hyperdrive@0.1.1
|
|
19
20
|
@qvac/error@0.1.1
|
|
20
21
|
@qvac/infer-base@0.1.1
|
|
22
|
+
@qvac/infer-base@0.4.0
|
|
23
|
+
https://github.com/tetherto/qvac
|
|
21
24
|
@qvac/logging@0.1.0
|
|
22
25
|
@qvac/response@0.1.2
|
|
23
26
|
adaptive-timeout@1.0.1
|
|
24
27
|
https://github.com/holepunchto/adaptive-timeout
|
|
25
|
-
b4a@1.
|
|
28
|
+
b4a@1.8.0
|
|
26
29
|
https://github.com/holepunchto/b4a
|
|
30
|
+
bare-abort@2.0.13
|
|
31
|
+
https://github.com/holepunchto/bare-abort
|
|
27
32
|
bare-addon-resolve@1.10.0
|
|
28
33
|
https://github.com/holepunchto/bare-addon-resolve
|
|
29
34
|
bare-ansi-escapes@2.2.3
|
|
30
35
|
https://github.com/holepunchto/bare-ansi-escapes
|
|
31
36
|
bare-assert@1.2.0
|
|
32
37
|
https://github.com/holepunchto/bare-assert
|
|
38
|
+
bare-buffer@3.6.0
|
|
39
|
+
https://github.com/holepunchto/bare-buffer
|
|
33
40
|
bare-channel@5.2.3
|
|
34
41
|
https://github.com/holepunchto/bare-channel
|
|
35
42
|
bare-env@3.0.0
|
|
@@ -38,9 +45,9 @@ JavaScript Dependencies
|
|
|
38
45
|
https://github.com/holepunchto/bare-events
|
|
39
46
|
bare-events@2.8.2
|
|
40
47
|
https://github.com/holepunchto/bare-events
|
|
41
|
-
bare-ffmpeg@1.
|
|
48
|
+
bare-ffmpeg@1.2.2
|
|
42
49
|
https://github.com/holepunchto/bare-ffmpeg
|
|
43
|
-
bare-fs@4.
|
|
50
|
+
bare-fs@4.7.1
|
|
44
51
|
https://github.com/holepunchto/bare-fs
|
|
45
52
|
bare-hrtime@2.1.1
|
|
46
53
|
https://github.com/holepunchto/bare-hrtime
|
|
@@ -48,59 +55,68 @@ JavaScript Dependencies
|
|
|
48
55
|
https://github.com/holepunchto/bare-inspect
|
|
49
56
|
bare-module-resolve@1.12.1
|
|
50
57
|
https://github.com/holepunchto/bare-module-resolve
|
|
51
|
-
bare-os@3.
|
|
58
|
+
bare-os@3.9.0
|
|
52
59
|
https://github.com/holepunchto/bare-os
|
|
53
60
|
bare-path@3.0.0
|
|
54
61
|
https://github.com/holepunchto/bare-path
|
|
55
|
-
bare-pipe@4.1.
|
|
62
|
+
bare-pipe@4.1.5
|
|
56
63
|
https://github.com/holepunchto/bare-pipe
|
|
57
|
-
bare-process@4.
|
|
64
|
+
bare-process@4.4.1
|
|
58
65
|
https://github.com/holepunchto/bare-process
|
|
59
|
-
bare-semver@1.0.
|
|
66
|
+
bare-semver@1.0.3
|
|
60
67
|
https://github.com/holepunchto/bare-semver
|
|
61
68
|
bare-signals@4.2.0
|
|
62
69
|
https://github.com/holepunchto/bare-signals
|
|
63
|
-
bare-
|
|
70
|
+
bare-stdio@1.0.2
|
|
71
|
+
https://github.com/holepunchto/bare-stdio
|
|
72
|
+
bare-stream@2.13.0
|
|
64
73
|
https://github.com/holepunchto/bare-stream
|
|
65
|
-
bare-structured-clone@1.5.
|
|
74
|
+
bare-structured-clone@1.5.4
|
|
66
75
|
https://github.com/holepunchto/bare-structured-clone
|
|
67
|
-
bare-tty@5.0
|
|
76
|
+
bare-tty@5.1.0
|
|
68
77
|
https://github.com/holepunchto/bare-tty
|
|
69
78
|
bare-type@1.1.0
|
|
70
79
|
https://github.com/holepunchto/bare-type
|
|
71
|
-
bare-url@2.
|
|
80
|
+
bare-url@2.4.2
|
|
72
81
|
https://github.com/holepunchto/bare-url
|
|
73
82
|
blind-relay@1.4.0
|
|
74
83
|
https://github.com/holepunchto/blind-relay
|
|
75
|
-
compact-encoding@2.
|
|
76
|
-
https://github.com/
|
|
84
|
+
compact-encoding@2.19.2
|
|
85
|
+
https://github.com/holepunchto/compact-encoding
|
|
77
86
|
device-file@2.3.1
|
|
78
87
|
https://github.com/holepunchto/device-file
|
|
79
88
|
events-universal@1.0.1
|
|
80
89
|
https://github.com/holepunchto/events-universal
|
|
81
90
|
fd-lock@2.1.1
|
|
82
91
|
https://github.com/holepunchto/fd-lock
|
|
83
|
-
fs-native-extensions@1.
|
|
92
|
+
fs-native-extensions@1.5.0
|
|
84
93
|
https://github.com/holepunchto/fs-native-extensions
|
|
85
|
-
hyperblobs@2.
|
|
94
|
+
hyperblobs@2.11.1
|
|
86
95
|
https://github.com/holepunchto/hyperblobs
|
|
87
96
|
hypercore-errors@1.5.0
|
|
88
97
|
https://github.com/holepunchto/hypercore-errors
|
|
89
98
|
hypercore-id-encoding@1.3.0
|
|
90
99
|
https://github.com/holepunchto/hypercore-id-encoding
|
|
91
|
-
hypercore-storage@2.
|
|
92
|
-
|
|
100
|
+
hypercore-storage@2.8.0
|
|
101
|
+
https://github.com/holepunchto/hypercore-storage
|
|
102
|
+
hyperdht-address@1.0.1
|
|
103
|
+
https://github.com/holepunchto/hyperdht-address
|
|
104
|
+
hyperdrive@13.3.2
|
|
93
105
|
https://github.com/holepunchto/hyperdrive
|
|
94
|
-
hyperschema@1.
|
|
106
|
+
hyperschema@1.20.1
|
|
95
107
|
https://github.com/holepunchto/hyperschema
|
|
96
|
-
index-encoder@3.
|
|
108
|
+
index-encoder@3.5.0
|
|
97
109
|
https://github.com/holepunchto/index-encoder
|
|
98
|
-
mirror-drive@1.
|
|
110
|
+
mirror-drive@1.14.2
|
|
99
111
|
https://github.com/holepunchto/mirror-drive
|
|
100
112
|
noise-handshake@4.2.0
|
|
101
113
|
https://github.com/holepunchto/noise-handshake
|
|
102
114
|
quickbit-native@2.4.8
|
|
103
115
|
https://github.com/holepunchto/quickbit-native
|
|
116
|
+
rabin-native@2.0.0
|
|
117
|
+
https://github.com/holepunchto/rabin-native
|
|
118
|
+
rabin-stream@2.0.0
|
|
119
|
+
https://github.com/holepunchto/rabin-stream
|
|
104
120
|
rache@1.0.0
|
|
105
121
|
https://github.com/holepunchto/rache
|
|
106
122
|
refcounter@1.0.0
|
|
@@ -109,7 +125,7 @@ JavaScript Dependencies
|
|
|
109
125
|
https://github.com/holepunchto/require-addon
|
|
110
126
|
resource-on-exit@1.0.0
|
|
111
127
|
https://github.com/holepunchto/bare-teardown
|
|
112
|
-
rocksdb-native@3.
|
|
128
|
+
rocksdb-native@3.15.0
|
|
113
129
|
https://github.com/holepunchto/rocksdb-native
|
|
114
130
|
scope-lock@1.2.4
|
|
115
131
|
https://github.com/holepunchto/scope-lock
|
|
@@ -153,12 +169,12 @@ JavaScript Dependencies
|
|
|
153
169
|
https://github.com/mafintosh/bogon
|
|
154
170
|
codecs@3.1.0
|
|
155
171
|
https://github.com/mafintosh/codecs
|
|
156
|
-
corestore@7.
|
|
172
|
+
corestore@7.9.2
|
|
157
173
|
https://github.com/holepunchto/corestore
|
|
158
174
|
debounceify@1.1.0
|
|
159
175
|
https://github.com/mafintosh/debounceify
|
|
160
|
-
dht-rpc@6.26.
|
|
161
|
-
https://github.com/
|
|
176
|
+
dht-rpc@6.26.4
|
|
177
|
+
https://github.com/holepunchto/dht-rpc
|
|
162
178
|
fast-fifo@1.3.2
|
|
163
179
|
https://github.com/mafintosh/fast-fifo
|
|
164
180
|
flat-tree@1.13.0
|
|
@@ -169,13 +185,13 @@ JavaScript Dependencies
|
|
|
169
185
|
https://github.com/mafintosh/generate-string
|
|
170
186
|
hyperbee@2.27.3
|
|
171
187
|
https://github.com/holepunchto/hyperbee
|
|
172
|
-
hypercore@11.
|
|
188
|
+
hypercore@11.28.1
|
|
173
189
|
https://github.com/holepunchto/hypercore
|
|
174
190
|
hypercore-crypto@3.6.1
|
|
175
191
|
https://github.com/mafintosh/hypercore-crypto
|
|
176
|
-
hyperdht@6.
|
|
192
|
+
hyperdht@6.30.0
|
|
177
193
|
https://github.com/holepunchto/hyperdht
|
|
178
|
-
hyperswarm@4.
|
|
194
|
+
hyperswarm@4.17.0
|
|
179
195
|
https://github.com/holepunchto/hyperswarm
|
|
180
196
|
is-options@1.0.2
|
|
181
197
|
https://github.com/mafintosh/is-options
|
|
@@ -189,8 +205,8 @@ JavaScript Dependencies
|
|
|
189
205
|
https://github.com/mafintosh/nat-sampler
|
|
190
206
|
protocol-buffers-encodings@1.2.0
|
|
191
207
|
https://github.com/mafintosh/protocol-buffers-encodings
|
|
192
|
-
protomux@3.10.
|
|
193
|
-
https://github.com/
|
|
208
|
+
protomux@3.10.3
|
|
209
|
+
https://github.com/holepunchto/protomux
|
|
194
210
|
queue-tick@1.0.1
|
|
195
211
|
https://github.com/mafintosh/queue-tick
|
|
196
212
|
random-array-iterator@1.0.0
|
|
@@ -201,7 +217,7 @@ JavaScript Dependencies
|
|
|
201
217
|
https://github.com/mafintosh/record-cache
|
|
202
218
|
resolve-reject-promise@1.1.0
|
|
203
219
|
https://github.com/mafintosh/resolve-reject-promise
|
|
204
|
-
safety-catch@1.0.
|
|
220
|
+
safety-catch@1.0.3
|
|
205
221
|
https://github.com/mafintosh/safety-catch
|
|
206
222
|
same-data@1.0.0
|
|
207
223
|
https://github.com/mafintosh/same-data
|
|
@@ -211,7 +227,7 @@ JavaScript Dependencies
|
|
|
211
227
|
https://github.com/mafintosh/signal-promise
|
|
212
228
|
signed-varint@2.0.1
|
|
213
229
|
https://github.com/dominictarr/signed-varint
|
|
214
|
-
sodium-native@5.0
|
|
230
|
+
sodium-native@5.1.0
|
|
215
231
|
https://github.com/holepunchto/sodium-native
|
|
216
232
|
sodium-secretstream@1.2.0
|
|
217
233
|
https://github.com/mafintosh/sodium-secretstream
|
|
@@ -219,7 +235,7 @@ JavaScript Dependencies
|
|
|
219
235
|
https://github.com/holepunchto/sodium-universal
|
|
220
236
|
speedometer@1.1.0
|
|
221
237
|
https://github.com/mafintosh/speedometer
|
|
222
|
-
streamx@2.
|
|
238
|
+
streamx@2.25.0
|
|
223
239
|
https://github.com/mafintosh/streamx
|
|
224
240
|
teex@1.0.1
|
|
225
241
|
https://github.com/mafintosh/teex
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# decoder-audio
|
|
2
2
|
|
|
3
3
|
This decoder library leverages FFmpeg for efficient audio decoding. It simplifies processing of input audio, particularly as a preprocessing step for other addons.
|
|
4
4
|
|
|
@@ -240,7 +240,7 @@ Coverage reports are generated in the 'coverage/unit/' directory. Open the corre
|
|
|
240
240
|
|
|
241
241
|
## Resources
|
|
242
242
|
|
|
243
|
-
* GitHub Repo: [tetherto/qvac](https://github.com/tetherto/qvac/tree/main/packages/
|
|
243
|
+
* GitHub Repo: [tetherto/qvac](https://github.com/tetherto/qvac/tree/main/packages/decoder-audio)
|
|
244
244
|
|
|
245
245
|
## License
|
|
246
246
|
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import QvacResponse from '@qvac/response'
|
|
1
|
+
import { QvacResponse } from '@qvac/infer-base'
|
|
3
2
|
|
|
4
3
|
interface AudioFormatConfig {
|
|
5
4
|
format: number | null
|
|
@@ -24,13 +23,6 @@ interface FFmpegDecoderConstructorParams {
|
|
|
24
23
|
streamIndex?: number
|
|
25
24
|
inputBitrate?: number
|
|
26
25
|
audioFormat?: 's16le' | 'f32le'
|
|
27
|
-
[key: string]: any
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface DecoderStatus {
|
|
31
|
-
loaded: boolean
|
|
32
|
-
active: boolean
|
|
33
|
-
paused: boolean
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
export interface DecoderOutput {
|
|
@@ -48,20 +40,16 @@ interface RuntimeStats {
|
|
|
48
40
|
audioFormat: 's16le' | 'f32le'
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
declare class FFmpegDecoder
|
|
43
|
+
declare class FFmpegDecoder {
|
|
52
44
|
SUPPORTED_AUDIO_FORMATS: SupportedAudioFormats
|
|
53
45
|
OUTPUT_CHANNEL_LAYOUT: number | null
|
|
54
46
|
|
|
55
|
-
constructor(params
|
|
47
|
+
constructor(params?: FFmpegDecoderConstructorParams)
|
|
56
48
|
|
|
57
49
|
load(): Promise<void>
|
|
58
50
|
unload(): Promise<void>
|
|
59
|
-
run(audioStream: AsyncIterable<Buffer>):
|
|
60
|
-
|
|
61
|
-
unpause(): Promise<void>
|
|
62
|
-
stop(): Promise<void>
|
|
63
|
-
status(): DecoderStatus
|
|
64
|
-
|
|
51
|
+
run(audioStream: AsyncIterable<Buffer>): QvacResponse<DecoderOutput>
|
|
52
|
+
|
|
65
53
|
runtimeStats(): RuntimeStats
|
|
66
54
|
}
|
|
67
55
|
|
package/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const QvacResponse = require('@qvac/response')
|
|
4
3
|
const QvacLogger = require('@qvac/logging')
|
|
5
4
|
const ffmpeg = require('bare-ffmpeg')
|
|
6
|
-
const
|
|
5
|
+
const { createJobHandler } = require('@qvac/infer-base')
|
|
6
|
+
const { QvacErrorDecoderAudio, ERR_CODES } = require('./utils/error')
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* FFmpeg-based audio decoder (single-threaded)
|
|
10
10
|
*/
|
|
11
|
-
class FFmpegDecoder
|
|
11
|
+
class FFmpegDecoder {
|
|
12
12
|
SUPPORTED_AUDIO_FORMATS = {
|
|
13
13
|
s16le: {
|
|
14
14
|
format: null, // Will be set to ffmpeg.constants.sampleFormats.S16
|
|
@@ -28,7 +28,6 @@ class FFmpegDecoder extends BaseInference {
|
|
|
28
28
|
* @param streamIndex - Index of the stream to decode. Default: 0
|
|
29
29
|
* @param inputBitrate - Input audio bitrate. Default: 192000
|
|
30
30
|
* @param audioFormat - Output audio format. Default: 's16le'
|
|
31
|
-
* @param args - Additional arguments passed to BaseInference
|
|
32
31
|
* @param {Object} [config.streamIndex] - Index of the stream to decode (default: 0)
|
|
33
32
|
* @param {number} [config.inputBitrate] - Input audio bitrate (default: 192000)
|
|
34
33
|
* @param {string} [config.audioFormat] - Output audio format (default: 'f32le')
|
|
@@ -40,11 +39,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
40
39
|
logger = null,
|
|
41
40
|
streamIndex = 0,
|
|
42
41
|
inputBitrate = 192000,
|
|
43
|
-
audioFormat = 's16le'
|
|
44
|
-
|
|
45
|
-
}) {
|
|
46
|
-
super({ ...args, logger })
|
|
47
|
-
|
|
42
|
+
audioFormat = 's16le'
|
|
43
|
+
} = {}) {
|
|
48
44
|
this.config = {
|
|
49
45
|
streamIndex: config.streamIndex || streamIndex,
|
|
50
46
|
inputBitrate: config.inputBitrate || inputBitrate,
|
|
@@ -54,7 +50,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
54
50
|
|
|
55
51
|
this.logger = new QvacLogger(logger)
|
|
56
52
|
this.isLoaded = false
|
|
57
|
-
this.
|
|
53
|
+
this._cancelled = false
|
|
54
|
+
this._job = createJobHandler({ cancel: () => this._cancelCurrent() })
|
|
58
55
|
|
|
59
56
|
// Encoder delay handling
|
|
60
57
|
this.samplesSkipped = 0
|
|
@@ -106,7 +103,10 @@ class FFmpegDecoder extends BaseInference {
|
|
|
106
103
|
|
|
107
104
|
// Validate audio format
|
|
108
105
|
if (!this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat]) {
|
|
109
|
-
throw new
|
|
106
|
+
throw new QvacErrorDecoderAudio({
|
|
107
|
+
code: ERR_CODES.UNSUPPORTED_AUDIO_FORMAT,
|
|
108
|
+
adds: this.config.audioFormat
|
|
109
|
+
})
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
this.isLoaded = true
|
|
@@ -124,7 +124,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
124
124
|
this.logger.info('Unloading FFmpegDecoder')
|
|
125
125
|
|
|
126
126
|
this.isLoaded = false
|
|
127
|
-
this.
|
|
127
|
+
this._cancelCurrent()
|
|
128
|
+
this._job.fail(new QvacErrorDecoderAudio({ code: ERR_CODES.DECODER_NOT_LOADED }))
|
|
128
129
|
this.logger.info('FFmpegDecoder unloaded')
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -133,41 +134,41 @@ class FFmpegDecoder extends BaseInference {
|
|
|
133
134
|
* @param {Readable} audioStream - Input audio stream
|
|
134
135
|
* @returns {QvacResponse} Response with decoded audio
|
|
135
136
|
*/
|
|
136
|
-
|
|
137
|
+
run (audioStream) {
|
|
137
138
|
if (!this.isLoaded) {
|
|
138
|
-
throw new
|
|
139
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.DECODER_NOT_LOADED })
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
this.logger.info('Starting new audio stream processing')
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
pauseHandler: () => this.pause(),
|
|
146
|
-
continueHandler: () => this.unpause()
|
|
147
|
-
})
|
|
144
|
+
this._cancelled = false
|
|
145
|
+
const response = this._job.start()
|
|
148
146
|
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.logger.error('Error processing audio stream:', err)
|
|
159
|
-
response.failed(err)
|
|
160
|
-
})
|
|
147
|
+
this._processStream(audioStream)
|
|
148
|
+
.then(() => {
|
|
149
|
+
this._job.end(this.runtimeStats())
|
|
150
|
+
})
|
|
151
|
+
.catch(err => {
|
|
152
|
+
this.logger.error('Error processing audio stream:', err)
|
|
153
|
+
this._job.active?.updateStats(this.runtimeStats())
|
|
154
|
+
this._job.fail(err)
|
|
155
|
+
})
|
|
161
156
|
|
|
162
157
|
return response
|
|
163
158
|
}
|
|
164
159
|
|
|
160
|
+
_cancelCurrent () {
|
|
161
|
+
this._cancelled = true
|
|
162
|
+
this.logger.debug('Decoder cancel requested')
|
|
163
|
+
return Promise.resolve()
|
|
164
|
+
}
|
|
165
|
+
|
|
165
166
|
_getBufferSize (inputBitrate) {
|
|
166
167
|
const maxBufferSize = 1024 * 1024 // 1MB max
|
|
167
168
|
return Math.min((inputBitrate / 8) * 4, maxBufferSize)
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
_processFrame (decoder, raw, resampler
|
|
171
|
+
_processFrame (decoder, raw, resampler) {
|
|
171
172
|
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
172
173
|
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
173
174
|
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
@@ -198,7 +199,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
198
199
|
const skipBytes = OUTPUT_FORMAT_BYTE_LENGTH * samplesToSkip * output.channelLayout.nbChannels
|
|
199
200
|
const length = OUTPUT_FORMAT_BYTE_LENGTH * (count - samplesToSkip) * output.channelLayout.nbChannels
|
|
200
201
|
const chunk = Buffer.from(samples.data.subarray(skipBytes, skipBytes + length))
|
|
201
|
-
|
|
202
|
+
this._job.output({ outputArray: chunk })
|
|
202
203
|
|
|
203
204
|
// Track stats for partial frame
|
|
204
205
|
this._runtimeStats.samplesDecoded += (count - samplesToSkip)
|
|
@@ -206,7 +207,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
206
207
|
} else {
|
|
207
208
|
const length = OUTPUT_FORMAT_BYTE_LENGTH * count * output.channelLayout.nbChannels
|
|
208
209
|
const chunk = Buffer.from(samples.data.subarray(0, length))
|
|
209
|
-
|
|
210
|
+
this._job.output({ outputArray: chunk })
|
|
210
211
|
|
|
211
212
|
// Track stats
|
|
212
213
|
this._runtimeStats.samplesDecoded += count
|
|
@@ -215,15 +216,19 @@ class FFmpegDecoder extends BaseInference {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
_processPacket (format, packet, raw, decoder, resampler
|
|
219
|
+
_processPacket (format, packet, raw, decoder, resampler) {
|
|
219
220
|
while (format.readFrame(packet)) {
|
|
221
|
+
if (this._cancelled) {
|
|
222
|
+
packet.unref()
|
|
223
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.JOB_CANCELLED })
|
|
224
|
+
}
|
|
220
225
|
decoder.sendPacket(packet)
|
|
221
|
-
this._processFrame(decoder, raw, resampler
|
|
226
|
+
this._processFrame(decoder, raw, resampler)
|
|
222
227
|
packet.unref()
|
|
223
228
|
}
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
_processFFmpegStream (format, stream
|
|
231
|
+
_processFFmpegStream (format, stream) {
|
|
227
232
|
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
228
233
|
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
229
234
|
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
@@ -266,7 +271,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
266
271
|
this.logger.info(`[FFmpegDecoder] Skipping ${skipMs}ms (${this.totalSkipSamples} samples) for ${codecName} to remove encoder artifacts`)
|
|
267
272
|
}
|
|
268
273
|
|
|
269
|
-
this._processPacket(format, packet, raw, decoder, resampler
|
|
274
|
+
this._processPacket(format, packet, raw, decoder, resampler)
|
|
270
275
|
|
|
271
276
|
// Flush resampler
|
|
272
277
|
const output = new ffmpeg.Frame()
|
|
@@ -286,7 +291,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
286
291
|
while ((flushCount = resampler.flush(output)) > 0) {
|
|
287
292
|
const actualLength = OUTPUT_FORMAT_BYTE_LENGTH * flushCount * output.channelLayout.nbChannels
|
|
288
293
|
const chunk = Buffer.from(samples.data.subarray(0, actualLength))
|
|
289
|
-
|
|
294
|
+
this._job.output({ outputArray: chunk })
|
|
290
295
|
|
|
291
296
|
// Track stats for flushed samples
|
|
292
297
|
this._runtimeStats.samplesDecoded += flushCount
|
|
@@ -296,19 +301,14 @@ class FFmpegDecoder extends BaseInference {
|
|
|
296
301
|
decoder.destroy()
|
|
297
302
|
}
|
|
298
303
|
|
|
299
|
-
async _collectStreamData (audioStream
|
|
304
|
+
async _collectStreamData (audioStream) {
|
|
300
305
|
const chunks = []
|
|
301
306
|
let totalBytes = 0
|
|
302
307
|
|
|
303
308
|
for await (const chunk of audioStream) {
|
|
304
|
-
if (
|
|
309
|
+
if (this._cancelled) {
|
|
305
310
|
this.logger.info('[FFmpegDecoder] Job cancelled, stopping stream collection')
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
while (job.isPaused) {
|
|
310
|
-
this.logger.debug('[FFmpegDecoder] Job is paused, waiting to resume...')
|
|
311
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
311
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.JOB_CANCELLED })
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
chunks.push(chunk)
|
|
@@ -320,154 +320,92 @@ class FFmpegDecoder extends BaseInference {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
async _processStream (audioStream) {
|
|
323
|
-
const job = this.currentJob
|
|
324
|
-
if (!job.isActive) {
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
|
|
328
323
|
// Reset and start tracking stats
|
|
329
324
|
this._resetStats()
|
|
330
325
|
const startTime = Date.now()
|
|
331
326
|
|
|
332
|
-
|
|
333
|
-
this.logger.info('[FFmpegDecoder] Starting stream processing')
|
|
327
|
+
this.logger.info('[FFmpegDecoder] Starting stream processing')
|
|
334
328
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
// Collect all audio data from stream
|
|
330
|
+
const audioBuffer = await this._collectStreamData(audioStream)
|
|
331
|
+
this.logger.info(`[FFmpegDecoder] Collected ${audioBuffer.length} bytes of audio data`)
|
|
338
332
|
|
|
339
|
-
|
|
340
|
-
|
|
333
|
+
// Track input bytes
|
|
334
|
+
this._runtimeStats.inputBytes = audioBuffer.length
|
|
341
335
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
336
|
+
if (this._cancelled) {
|
|
337
|
+
this.logger.info('[FFmpegDecoder] Job cancelled after data collection')
|
|
338
|
+
this._runtimeStats.decodeTimeMs = Date.now() - startTime
|
|
339
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.JOB_CANCELLED })
|
|
340
|
+
}
|
|
346
341
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const io = new ffmpeg.IOContext(bufferSize, {
|
|
352
|
-
onread: (buffer, requestedLen) => {
|
|
353
|
-
const remainingBytes = audioBuffer.length - bufferOffset
|
|
354
|
-
const bytesToRead = Math.min(requestedLen, remainingBytes)
|
|
355
|
-
|
|
356
|
-
if (bytesToRead <= 0) {
|
|
357
|
-
return 0 // EOF
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
|
|
361
|
-
bufferOffset += bytesToRead
|
|
362
|
-
|
|
363
|
-
this.logger.debug(`[FFmpegDecoder] Read ${bytesToRead} bytes from buffer, offset now: ${bufferOffset}`)
|
|
364
|
-
return bytesToRead
|
|
365
|
-
},
|
|
366
|
-
onseek: (offset, whence) => {
|
|
367
|
-
const AVSEEK_SIZE = 0x10000
|
|
368
|
-
|
|
369
|
-
if (whence === AVSEEK_SIZE) {
|
|
370
|
-
return audioBuffer.length
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
let newOffset
|
|
374
|
-
if (whence === 0) {
|
|
375
|
-
newOffset = offset
|
|
376
|
-
} else if (whence === 1) {
|
|
377
|
-
newOffset = bufferOffset + offset
|
|
378
|
-
} else if (whence === 2) {
|
|
379
|
-
newOffset = audioBuffer.length + offset
|
|
380
|
-
} else {
|
|
381
|
-
return -1
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (newOffset < 0 || newOffset > audioBuffer.length) {
|
|
385
|
-
return -1
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
bufferOffset = newOffset
|
|
389
|
-
this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
|
|
390
|
-
return bufferOffset
|
|
391
|
-
}
|
|
392
|
-
})
|
|
342
|
+
// Create FFmpeg IO context with the buffer
|
|
343
|
+
const bufferSize = this._getBufferSize(this.config.inputBitrate)
|
|
344
|
+
let bufferOffset = 0
|
|
393
345
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
346
|
+
const io = new ffmpeg.IOContext(bufferSize, {
|
|
347
|
+
onread: (buffer, requestedLen) => {
|
|
348
|
+
const remainingBytes = audioBuffer.length - bufferOffset
|
|
349
|
+
const bytesToRead = Math.min(requestedLen, remainingBytes)
|
|
397
350
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
351
|
+
if (bytesToRead <= 0) {
|
|
352
|
+
return 0 // EOF
|
|
353
|
+
}
|
|
402
354
|
|
|
403
|
-
|
|
404
|
-
|
|
355
|
+
audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
|
|
356
|
+
bufferOffset += bytesToRead
|
|
405
357
|
|
|
406
|
-
|
|
407
|
-
|
|
358
|
+
this.logger.debug(`[FFmpegDecoder] Read ${bytesToRead} bytes from buffer, offset now: ${bufferOffset}`)
|
|
359
|
+
return bytesToRead
|
|
360
|
+
},
|
|
361
|
+
onseek: (offset, whence) => {
|
|
362
|
+
const AVSEEK_SIZE = 0x10000
|
|
363
|
+
|
|
364
|
+
if (whence === AVSEEK_SIZE) {
|
|
365
|
+
return audioBuffer.length
|
|
366
|
+
}
|
|
408
367
|
|
|
409
|
-
|
|
410
|
-
|
|
368
|
+
let newOffset
|
|
369
|
+
if (whence === 0) {
|
|
370
|
+
newOffset = offset
|
|
371
|
+
} else if (whence === 1) {
|
|
372
|
+
newOffset = bufferOffset + offset
|
|
373
|
+
} else if (whence === 2) {
|
|
374
|
+
newOffset = audioBuffer.length + offset
|
|
375
|
+
} else {
|
|
376
|
+
return -1
|
|
377
|
+
}
|
|
411
378
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this.logger.info(`[FFmpegDecoder] Runtime stats: ${JSON.stringify(this._runtimeStats)}`)
|
|
416
|
-
} catch (err) {
|
|
417
|
-
// Still capture stats even on error
|
|
418
|
-
this._runtimeStats.decodeTimeMs = Date.now() - startTime
|
|
419
|
-
job.response.updateStats(this.runtimeStats())
|
|
379
|
+
if (newOffset < 0 || newOffset > audioBuffer.length) {
|
|
380
|
+
return -1
|
|
381
|
+
}
|
|
420
382
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
383
|
+
bufferOffset = newOffset
|
|
384
|
+
this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
|
|
385
|
+
return bufferOffset
|
|
386
|
+
}
|
|
387
|
+
})
|
|
424
388
|
|
|
425
|
-
this.logger.
|
|
426
|
-
|
|
389
|
+
this.logger.debug('[FFmpegDecoder] IOContext created')
|
|
390
|
+
const format = new ffmpeg.InputFormatContext(io)
|
|
391
|
+
this.logger.debug('[FFmpegDecoder] InputFormatContext created')
|
|
427
392
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
this.logger.debug('Decoder paused')
|
|
393
|
+
const streamIndex = this.config.streamIndex || 0
|
|
394
|
+
if (format.streams[streamIndex] === undefined) {
|
|
395
|
+
throw new QvacErrorDecoderAudio({
|
|
396
|
+
code: ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS,
|
|
397
|
+
adds: streamIndex
|
|
398
|
+
})
|
|
435
399
|
}
|
|
436
|
-
return Promise.resolve()
|
|
437
|
-
}
|
|
438
400
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
*/
|
|
442
|
-
unpause () {
|
|
443
|
-
if (this.currentJob) {
|
|
444
|
-
this.currentJob.isPaused = false
|
|
445
|
-
this.logger.debug('Decoder unpaused')
|
|
446
|
-
}
|
|
447
|
-
return Promise.resolve()
|
|
448
|
-
}
|
|
401
|
+
// Process the stream and generate decoded output
|
|
402
|
+
this._processFFmpegStream(format, format.streams[streamIndex])
|
|
449
403
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
*/
|
|
453
|
-
stop () {
|
|
454
|
-
if (this.currentJob) {
|
|
455
|
-
this.currentJob.isActive = false
|
|
456
|
-
this.currentJob.response.finish()
|
|
457
|
-
this.logger.debug('Decoder stopped')
|
|
458
|
-
}
|
|
459
|
-
return Promise.resolve()
|
|
460
|
-
}
|
|
404
|
+
// Calculate final decode time
|
|
405
|
+
this._runtimeStats.decodeTimeMs = Date.now() - startTime
|
|
461
406
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
*/
|
|
465
|
-
status () {
|
|
466
|
-
return {
|
|
467
|
-
loaded: this.isLoaded,
|
|
468
|
-
active: this.currentJob?.isActive || false,
|
|
469
|
-
paused: this.currentJob?.isPaused || false
|
|
470
|
-
}
|
|
407
|
+
this.logger.info('[FFmpegDecoder] Stream processing completed successfully')
|
|
408
|
+
this.logger.info(`[FFmpegDecoder] Runtime stats: ${JSON.stringify(this._runtimeStats)}`)
|
|
471
409
|
}
|
|
472
410
|
}
|
|
473
411
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/decoder-audio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Tether",
|
|
@@ -46,23 +46,21 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@qvac/error": "^0.1.0",
|
|
49
|
-
"@qvac/infer-base": "^0.
|
|
49
|
+
"@qvac/infer-base": "^0.4.0",
|
|
50
50
|
"@qvac/logging": "^0.1.0",
|
|
51
|
-
"@qvac/response": "^0.1.0",
|
|
52
51
|
"bare-assert": "^1.1.0",
|
|
53
52
|
"bare-channel": "^5.2.2",
|
|
54
53
|
"bare-ffmpeg": "^1.0.0-32",
|
|
55
54
|
"bare-fs": "^4.5.1",
|
|
56
55
|
"bare-path": "^3.0.0",
|
|
57
|
-
"bare-process": "^4.2.2"
|
|
58
|
-
"process": "npm:bare-process@^4.2.2"
|
|
56
|
+
"bare-process": "^4.2.2"
|
|
59
57
|
},
|
|
60
58
|
"repository": {
|
|
61
59
|
"type": "git",
|
|
62
60
|
"url": "git+https://github.com/tetherto/qvac.git",
|
|
63
|
-
"directory": "packages/
|
|
61
|
+
"directory": "packages/decoder-audio"
|
|
64
62
|
},
|
|
65
63
|
"bugs": "https://github.com/tetherto/qvac/issues",
|
|
66
|
-
"homepage": "https://github.com/tetherto/qvac/tree/main/packages/
|
|
64
|
+
"homepage": "https://github.com/tetherto/qvac/tree/main/packages/decoder-audio#readme",
|
|
67
65
|
"types": "index.d.ts"
|
|
68
66
|
}
|
|
@@ -22,7 +22,7 @@ function createStreamAccumulator ({
|
|
|
22
22
|
targetBufferSize = TARGET_BUFFER_SIZE
|
|
23
23
|
}) {
|
|
24
24
|
if (targetBufferSize < TARGET_BUFFER_SIZE) {
|
|
25
|
-
throw new QvacErrorDecoderAudio(ERR_CODES.BUFFER_SIZE_TOO_SMALL)
|
|
25
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.BUFFER_SIZE_TOO_SMALL })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
let accumulator = Buffer.alloc(0)
|
package/utils/error.js
CHANGED
|
@@ -14,7 +14,11 @@ const ERR_CODES = Object.freeze({
|
|
|
14
14
|
FAILED_TO_APPEND: 11005,
|
|
15
15
|
FAILED_TO_GET_STATUS: 11006,
|
|
16
16
|
FAILED_TO_DESTROY: 11007,
|
|
17
|
-
BUFFER_SIZE_TOO_SMALL: 11008
|
|
17
|
+
BUFFER_SIZE_TOO_SMALL: 11008,
|
|
18
|
+
UNSUPPORTED_AUDIO_FORMAT: 11009,
|
|
19
|
+
DECODER_NOT_LOADED: 11010,
|
|
20
|
+
STREAM_INDEX_OUT_OF_BOUNDS: 11011,
|
|
21
|
+
JOB_CANCELLED: 11012
|
|
18
22
|
})
|
|
19
23
|
|
|
20
24
|
addCodes({
|
|
@@ -49,6 +53,22 @@ addCodes({
|
|
|
49
53
|
[ERR_CODES.BUFFER_SIZE_TOO_SMALL]: {
|
|
50
54
|
name: 'BUFFER_SIZE_TOO_SMALL',
|
|
51
55
|
message: 'Target buffer size is too small'
|
|
56
|
+
},
|
|
57
|
+
[ERR_CODES.UNSUPPORTED_AUDIO_FORMAT]: {
|
|
58
|
+
name: 'UNSUPPORTED_AUDIO_FORMAT',
|
|
59
|
+
message: (format) => `Unsupported audio format: ${format}`
|
|
60
|
+
},
|
|
61
|
+
[ERR_CODES.DECODER_NOT_LOADED]: {
|
|
62
|
+
name: 'DECODER_NOT_LOADED',
|
|
63
|
+
message: 'Decoder not loaded. Call load() first.'
|
|
64
|
+
},
|
|
65
|
+
[ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS]: {
|
|
66
|
+
name: 'STREAM_INDEX_OUT_OF_BOUNDS',
|
|
67
|
+
message: (index) => `Stream index out of bounds: ${index}`
|
|
68
|
+
},
|
|
69
|
+
[ERR_CODES.JOB_CANCELLED]: {
|
|
70
|
+
name: 'JOB_CANCELLED',
|
|
71
|
+
message: 'Decoder job cancelled'
|
|
52
72
|
}
|
|
53
73
|
}, {
|
|
54
74
|
name,
|