@qvac/decoder-audio 0.2.10 → 0.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/README.md +15 -43
- package/index.d.ts +34 -52
- package/index.js +369 -75
- package/package.json +1 -8
- package/test/mobile/testAssets/corrupted.mp3 +0 -0
- package/utils/error.js +1 -1
- package/binding.js +0 -3
- package/gstreamer.js +0 -131
- package/lib/ffmpeg/ffmpeg-decoder.js +0 -413
- package/lib/ffmpeg/index.d.ts +0 -98
- package/lib/ffmpeg/index.js +0 -29
- package/prebuilds/android-arm/qvac__decoder-audio.bare +0 -0
- package/prebuilds/android-arm64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/android-ia32/qvac__decoder-audio.bare +0 -0
- package/prebuilds/android-x64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__decoder-audio.bare.exports +0 -7163
- package/prebuilds/ios-arm64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/ios-arm64/qvac__decoder-audio.bare.exports +0 -7163
- package/prebuilds/ios-arm64-simulator/qvac__decoder-audio.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__decoder-audio.bare.exports +0 -7163
- package/prebuilds/ios-x64-simulator/qvac__decoder-audio.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__decoder-audio.bare.exports +0 -7141
- package/prebuilds/linux-x64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/win32-x64/qvac__decoder-audio.bare +0 -0
- package/prebuilds/win32-x64/qvac__decoder-audio.bare.exports +0 -0
- package/test/mobile/gst-test.cjs +0 -103
package/index.js
CHANGED
|
@@ -1,119 +1,413 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const QvacResponse = require('@qvac/response')
|
|
4
|
+
const QvacLogger = require('@qvac/logging')
|
|
5
|
+
const ffmpeg = require('bare-ffmpeg')
|
|
4
6
|
const BaseInference = require('@qvac/infer-base/WeightsProvider/BaseInference')
|
|
5
|
-
const { GSTDecoderInterface } = require('./gstreamer')
|
|
6
|
-
const createStreamAccumulator = require('./utils/createStreamAccumulator')
|
|
7
|
-
const { FORMATS_NEEDING_DECODE, SUPPORTED_AUDIO_FORMATS } = require('./constants')
|
|
8
|
-
|
|
9
|
-
const END_OF_INPUT = 'end of job'
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
9
|
+
* FFmpeg-based audio decoder (single-threaded)
|
|
13
10
|
*/
|
|
14
|
-
class
|
|
11
|
+
class FFmpegDecoder extends BaseInference {
|
|
12
|
+
SUPPORTED_AUDIO_FORMATS = {
|
|
13
|
+
s16le: {
|
|
14
|
+
format: null, // Will be set to ffmpeg.constants.sampleFormats.S16
|
|
15
|
+
byteLength: 2
|
|
16
|
+
},
|
|
17
|
+
f32le: {
|
|
18
|
+
format: null, // Will be set to ffmpeg.constants.sampleFormats.FLT
|
|
19
|
+
byteLength: 4
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
OUTPUT_CHANNEL_LAYOUT = null // Will be set to ffmpeg.constants.channelLayouts.MONO
|
|
15
24
|
/**
|
|
16
|
-
* Creates an instance of
|
|
17
|
-
* @
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
* @param
|
|
25
|
+
* Creates an instance of FFmpegDecoder.
|
|
26
|
+
* @param {Object} config - Configuration options
|
|
27
|
+
* @param logger - Logger instance
|
|
28
|
+
* @param streamIndex - Index of the stream to decode. Default: 0
|
|
29
|
+
* @param inputBitrate - Input audio bitrate. Default: 192000
|
|
30
|
+
* @param audioFormat - Output audio format. Default: 's16le'
|
|
31
|
+
* @param args - Additional arguments passed to BaseInference
|
|
32
|
+
* @param {Object} [config.streamIndex] - Index of the stream to decode (default: 0)
|
|
33
|
+
* @param {number} [config.inputBitrate] - Input audio bitrate (default: 192000)
|
|
34
|
+
* @param {string} [config.audioFormat] - Output audio format (default: 'f32le')
|
|
35
|
+
* @param {number} [config.sampleRate] - Output sample rate (default: 16000)
|
|
36
|
+
* @param {Object} [config.logger] - Logger instance
|
|
21
37
|
*/
|
|
38
|
+
constructor ({
|
|
39
|
+
config = {},
|
|
40
|
+
logger = null,
|
|
41
|
+
streamIndex = 0,
|
|
42
|
+
inputBitrate = 192000,
|
|
43
|
+
audioFormat = 's16le',
|
|
44
|
+
...args
|
|
45
|
+
}) {
|
|
46
|
+
super({ ...args, logger })
|
|
47
|
+
|
|
48
|
+
this.config = {
|
|
49
|
+
streamIndex: config.streamIndex || streamIndex,
|
|
50
|
+
inputBitrate: config.inputBitrate || inputBitrate,
|
|
51
|
+
audioFormat: config.audioFormat || audioFormat,
|
|
52
|
+
sampleRate: config.sampleRate || 16000
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.logger = new QvacLogger(logger)
|
|
56
|
+
this.isLoaded = false
|
|
57
|
+
this.currentJob = null
|
|
22
58
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.
|
|
59
|
+
// Encoder delay handling
|
|
60
|
+
this.samplesSkipped = 0
|
|
61
|
+
this.totalSkipSamples = 0
|
|
26
62
|
}
|
|
27
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Load and initialize the decoder
|
|
66
|
+
*/
|
|
28
67
|
async load () {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
if (this.isLoaded) {
|
|
69
|
+
this.logger.info('FFmpegDecoder already loaded')
|
|
70
|
+
return
|
|
32
71
|
}
|
|
33
72
|
|
|
34
|
-
this.logger.info('Loading
|
|
35
|
-
this.addon = this.createAddon(config)
|
|
73
|
+
this.logger.info('Loading FFmpegDecoder with config:', this.config)
|
|
36
74
|
|
|
37
|
-
|
|
38
|
-
this.
|
|
75
|
+
// Initialize format constants
|
|
76
|
+
this.SUPPORTED_AUDIO_FORMATS.s16le.format = ffmpeg.constants.sampleFormats.S16
|
|
77
|
+
this.SUPPORTED_AUDIO_FORMATS.f32le.format = ffmpeg.constants.sampleFormats.FLT
|
|
78
|
+
this.OUTPUT_CHANNEL_LAYOUT = ffmpeg.constants.channelLayouts.MONO
|
|
79
|
+
|
|
80
|
+
// Validate audio format
|
|
81
|
+
if (!this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat]) {
|
|
82
|
+
throw new Error(`Unsupported audio format: ${this.config.audioFormat}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.isLoaded = true
|
|
86
|
+
this.logger.info('FFmpegDecoder loaded successfully')
|
|
39
87
|
}
|
|
40
88
|
|
|
41
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Unload the decoder and clean up resources
|
|
91
|
+
*/
|
|
92
|
+
async unload () {
|
|
93
|
+
if (!this.isLoaded) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.logger.info('Unloading FFmpegDecoder')
|
|
98
|
+
|
|
99
|
+
this.isLoaded = false
|
|
100
|
+
this.currentJob = null
|
|
101
|
+
this.logger.info('FFmpegDecoder unloaded')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run the decoder on an audio stream
|
|
106
|
+
* @param {Readable} audioStream - Input audio stream
|
|
107
|
+
* @returns {QvacResponse} Response with decoded audio
|
|
108
|
+
*/
|
|
109
|
+
async run (audioStream) {
|
|
110
|
+
if (!this.isLoaded) {
|
|
111
|
+
throw new Error('Decoder not loaded. Call load() first.')
|
|
112
|
+
}
|
|
113
|
+
|
|
42
114
|
this.logger.info('Starting new audio stream processing')
|
|
43
|
-
const response = new QvacResponse({
|
|
44
|
-
cancelHandler: () => this.addon.cancel(jobId),
|
|
45
|
-
pauseHandler: () => this.addon.pause(),
|
|
46
|
-
continueHandler: () => this.addon.continue()
|
|
47
|
-
})
|
|
48
115
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
116
|
+
const response = new QvacResponse({
|
|
117
|
+
cancelHandler: () => this.stop(),
|
|
118
|
+
pauseHandler: () => this.pause(),
|
|
119
|
+
continueHandler: () => this.unpause()
|
|
53
120
|
})
|
|
54
121
|
|
|
55
|
-
this.
|
|
56
|
-
|
|
122
|
+
this.currentJob = {
|
|
123
|
+
response,
|
|
124
|
+
audioChunks: [],
|
|
125
|
+
isActive: true,
|
|
126
|
+
isPaused: false
|
|
127
|
+
}
|
|
57
128
|
|
|
58
|
-
|
|
129
|
+
// Process the audio stream
|
|
130
|
+
this._processStream(audioStream).catch(err => {
|
|
59
131
|
this.logger.error('Error processing audio stream:', err)
|
|
60
132
|
response.failed(err)
|
|
61
133
|
})
|
|
134
|
+
|
|
62
135
|
return response
|
|
63
136
|
}
|
|
64
137
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
138
|
+
_getBufferSize (inputBitrate) {
|
|
139
|
+
const maxBufferSize = 1024 * 1024 // 1MB max
|
|
140
|
+
return Math.min((inputBitrate / 8) * 4, maxBufferSize)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_processFrame (decoder, raw, resampler, job) {
|
|
144
|
+
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
145
|
+
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
146
|
+
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
147
|
+
|
|
148
|
+
while (decoder.receiveFrame(raw)) {
|
|
149
|
+
const output = new ffmpeg.Frame()
|
|
150
|
+
output.channelLayout = this.OUTPUT_CHANNEL_LAYOUT
|
|
151
|
+
output.format = OUTPUT_FORMAT
|
|
152
|
+
output.sampleRate = OUTPUT_SAMPLE_RATE
|
|
153
|
+
output.nbSamples = raw.nbSamples
|
|
154
|
+
|
|
155
|
+
const samples = new ffmpeg.Samples(
|
|
156
|
+
output.format,
|
|
157
|
+
output.channelLayout.nbChannels,
|
|
158
|
+
output.nbSamples
|
|
159
|
+
)
|
|
160
|
+
samples.fill(output)
|
|
161
|
+
|
|
162
|
+
const count = resampler.convert(raw, output)
|
|
163
|
+
|
|
164
|
+
// Handle encoder delay by skipping initial samples
|
|
165
|
+
if (this.samplesSkipped < this.totalSkipSamples) {
|
|
166
|
+
const samplesToSkip = Math.min(count, this.totalSkipSamples - this.samplesSkipped)
|
|
167
|
+
this.samplesSkipped += samplesToSkip
|
|
168
|
+
if (samplesToSkip >= count) continue // Skip entire frame
|
|
169
|
+
|
|
170
|
+
// Skip partial frame
|
|
171
|
+
const skipBytes = OUTPUT_FORMAT_BYTE_LENGTH * samplesToSkip * output.channelLayout.nbChannels
|
|
172
|
+
const length = OUTPUT_FORMAT_BYTE_LENGTH * (count - samplesToSkip) * output.channelLayout.nbChannels
|
|
173
|
+
const chunk = Buffer.from(samples.data.subarray(skipBytes, skipBytes + length))
|
|
174
|
+
job.response.updateOutput({ outputArray: chunk })
|
|
175
|
+
} else {
|
|
176
|
+
const length = OUTPUT_FORMAT_BYTE_LENGTH * count * output.channelLayout.nbChannels
|
|
177
|
+
const chunk = Buffer.from(samples.data.subarray(0, length))
|
|
178
|
+
job.response.updateOutput({ outputArray: chunk })
|
|
79
179
|
}
|
|
80
|
-
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_processPacket (format, packet, raw, decoder, resampler, job) {
|
|
184
|
+
while (format.readFrame(packet)) {
|
|
185
|
+
decoder.sendPacket(packet)
|
|
186
|
+
this._processFrame(decoder, raw, resampler, job)
|
|
187
|
+
packet.unref()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_processFFmpegStream (format, stream, job) {
|
|
192
|
+
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
193
|
+
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
194
|
+
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
195
|
+
|
|
196
|
+
this.logger.debug('[FFmpegDecoder] Stream codec:', stream.codec, stream.codecParameters)
|
|
197
|
+
|
|
198
|
+
const packet = new ffmpeg.Packet()
|
|
199
|
+
const raw = new ffmpeg.Frame()
|
|
200
|
+
|
|
201
|
+
const resampler = new ffmpeg.Resampler(
|
|
202
|
+
stream.codecParameters.sampleRate,
|
|
203
|
+
stream.codecParameters.channelLayout,
|
|
204
|
+
stream.codecParameters.format,
|
|
205
|
+
OUTPUT_SAMPLE_RATE,
|
|
206
|
+
this.OUTPUT_CHANNEL_LAYOUT,
|
|
207
|
+
OUTPUT_FORMAT
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const decoder = stream.decoder()
|
|
211
|
+
decoder.open()
|
|
212
|
+
|
|
213
|
+
// Auto-detect encoder delay: lossy codecs need ~400ms skipped to remove artifacts
|
|
214
|
+
const codecName = stream.codec.name.toLowerCase()
|
|
215
|
+
const SKIP_MS = {
|
|
216
|
+
mp3: 400,
|
|
217
|
+
vorbis: 400,
|
|
218
|
+
opus: 150,
|
|
219
|
+
aac: 300
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const skipMs = SKIP_MS[codecName] || 0
|
|
223
|
+
this.samplesSkipped = 0
|
|
224
|
+
this.totalSkipSamples = Math.floor((OUTPUT_SAMPLE_RATE * skipMs) / 1000)
|
|
225
|
+
|
|
226
|
+
if (this.totalSkipSamples > 0) {
|
|
227
|
+
this.logger.info(`[FFmpegDecoder] Skipping ${skipMs}ms (${this.totalSkipSamples} samples) for ${codecName} to remove encoder artifacts`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this._processPacket(format, packet, raw, decoder, resampler, job)
|
|
231
|
+
|
|
232
|
+
// Flush resampler
|
|
233
|
+
const output = new ffmpeg.Frame()
|
|
234
|
+
output.channelLayout = this.OUTPUT_CHANNEL_LAYOUT
|
|
235
|
+
output.format = OUTPUT_FORMAT
|
|
236
|
+
output.sampleRate = OUTPUT_SAMPLE_RATE
|
|
237
|
+
output.nbSamples = 1024
|
|
238
|
+
|
|
239
|
+
const samples = new ffmpeg.Samples(
|
|
240
|
+
output.format,
|
|
241
|
+
output.channelLayout.nbChannels,
|
|
242
|
+
output.nbSamples
|
|
243
|
+
)
|
|
244
|
+
samples.fill(output)
|
|
245
|
+
|
|
246
|
+
let flushCount
|
|
247
|
+
while ((flushCount = resampler.flush(output)) > 0) {
|
|
248
|
+
const actualLength = OUTPUT_FORMAT_BYTE_LENGTH * flushCount * output.channelLayout.nbChannels
|
|
249
|
+
const chunk = Buffer.from(samples.data.subarray(0, actualLength))
|
|
250
|
+
job.response.updateOutput({ outputArray: chunk })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
decoder.destroy()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async _collectStreamData (audioStream, job) {
|
|
257
|
+
const chunks = []
|
|
258
|
+
let totalBytes = 0
|
|
81
259
|
|
|
82
260
|
for await (const chunk of audioStream) {
|
|
83
|
-
|
|
261
|
+
if (!job.isActive) {
|
|
262
|
+
this.logger.info('[FFmpegDecoder] Job cancelled, stopping stream collection')
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
while (job.isPaused) {
|
|
267
|
+
this.logger.debug('[FFmpegDecoder] Job is paused, waiting to resume...')
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
chunks.push(chunk)
|
|
272
|
+
totalBytes += chunk.length
|
|
273
|
+
this.logger.debug(`[FFmpegDecoder] Collected chunk, total bytes: ${totalBytes}`)
|
|
84
274
|
}
|
|
85
275
|
|
|
86
|
-
|
|
87
|
-
this.logger.info('Audio stream handling completed')
|
|
276
|
+
return Buffer.concat(chunks)
|
|
88
277
|
}
|
|
89
278
|
|
|
90
|
-
async
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
279
|
+
async _processStream (audioStream) {
|
|
280
|
+
const job = this.currentJob
|
|
281
|
+
if (!job.isActive) {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
this.logger.info('[FFmpegDecoder] Starting stream processing')
|
|
287
|
+
|
|
288
|
+
// Collect all audio data from stream
|
|
289
|
+
const audioBuffer = await this._collectStreamData(audioStream, job)
|
|
290
|
+
this.logger.info(`[FFmpegDecoder] Collected ${audioBuffer.length} bytes of audio data`)
|
|
291
|
+
|
|
292
|
+
if (!job.isActive) {
|
|
293
|
+
this.logger.info('[FFmpegDecoder] Job cancelled after data collection')
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create FFmpeg IO context with the buffer
|
|
298
|
+
const bufferSize = this._getBufferSize(this.config.inputBitrate)
|
|
299
|
+
let bufferOffset = 0
|
|
300
|
+
|
|
301
|
+
const io = new ffmpeg.IOContext(bufferSize, {
|
|
302
|
+
onread: (buffer, requestedLen) => {
|
|
303
|
+
const remainingBytes = audioBuffer.length - bufferOffset
|
|
304
|
+
const bytesToRead = Math.min(requestedLen, remainingBytes)
|
|
305
|
+
|
|
306
|
+
if (bytesToRead <= 0) {
|
|
307
|
+
return 0 // EOF
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
|
|
311
|
+
bufferOffset += bytesToRead
|
|
312
|
+
|
|
313
|
+
this.logger.debug(`[FFmpegDecoder] Read ${bytesToRead} bytes from buffer, offset now: ${bufferOffset}`)
|
|
314
|
+
return bytesToRead
|
|
315
|
+
},
|
|
316
|
+
onseek: (offset, whence) => {
|
|
317
|
+
const AVSEEK_SIZE = 0x10000
|
|
318
|
+
|
|
319
|
+
if (whence === AVSEEK_SIZE) {
|
|
320
|
+
return audioBuffer.length
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let newOffset
|
|
324
|
+
if (whence === 0) {
|
|
325
|
+
newOffset = offset
|
|
326
|
+
} else if (whence === 1) {
|
|
327
|
+
newOffset = bufferOffset + offset
|
|
328
|
+
} else if (whence === 2) {
|
|
329
|
+
newOffset = audioBuffer.length + offset
|
|
330
|
+
} else {
|
|
331
|
+
return -1
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (newOffset < 0 || newOffset > audioBuffer.length) {
|
|
335
|
+
return -1
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
bufferOffset = newOffset
|
|
339
|
+
this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
|
|
340
|
+
return bufferOffset
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
this.logger.debug('[FFmpegDecoder] IOContext created')
|
|
345
|
+
const format = new ffmpeg.InputFormatContext(io)
|
|
346
|
+
this.logger.debug('[FFmpegDecoder] InputFormatContext created')
|
|
347
|
+
|
|
348
|
+
const streamIndex = this.config.streamIndex || 0
|
|
349
|
+
if (format.streams[streamIndex] === undefined) {
|
|
350
|
+
throw new Error('Stream index out of bounds')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Process the stream and generate decoded output
|
|
354
|
+
this._processFFmpegStream(format, format.streams[streamIndex], job)
|
|
355
|
+
|
|
356
|
+
// Mark as complete
|
|
357
|
+
job.response.ended()
|
|
358
|
+
this.logger.info('[FFmpegDecoder] Stream processing completed successfully')
|
|
359
|
+
} catch (err) {
|
|
360
|
+
this.logger.error('Error processing audio stream:', err)
|
|
361
|
+
job.response.failed(err)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.logger.info('Audio _processStream completed')
|
|
94
365
|
}
|
|
95
366
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Pause the current job
|
|
369
|
+
*/
|
|
370
|
+
pause () {
|
|
371
|
+
if (this.currentJob) {
|
|
372
|
+
this.currentJob.isPaused = true
|
|
373
|
+
this.logger.debug('Decoder paused')
|
|
101
374
|
}
|
|
375
|
+
return Promise.resolve()
|
|
102
376
|
}
|
|
103
377
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Unpause the current job
|
|
380
|
+
*/
|
|
381
|
+
unpause () {
|
|
382
|
+
if (this.currentJob) {
|
|
383
|
+
this.currentJob.isPaused = false
|
|
384
|
+
this.logger.debug('Decoder unpaused')
|
|
385
|
+
}
|
|
386
|
+
return Promise.resolve()
|
|
111
387
|
}
|
|
112
|
-
}
|
|
113
388
|
|
|
114
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Stop the current job
|
|
391
|
+
*/
|
|
392
|
+
stop () {
|
|
393
|
+
if (this.currentJob) {
|
|
394
|
+
this.currentJob.isActive = false
|
|
395
|
+
this.currentJob.response.finish()
|
|
396
|
+
this.logger.debug('Decoder stopped')
|
|
397
|
+
}
|
|
398
|
+
return Promise.resolve()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get the current status
|
|
403
|
+
*/
|
|
404
|
+
status () {
|
|
405
|
+
return {
|
|
406
|
+
loaded: this.isLoaded,
|
|
407
|
+
active: this.currentJob?.isActive || false,
|
|
408
|
+
paused: this.currentJob?.isPaused || false
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
115
412
|
|
|
116
|
-
module.exports =
|
|
117
|
-
module.exports.FORMATS_NEEDING_DECODE = FORMATS_NEEDING_DECODE
|
|
118
|
-
module.exports.SUPPORTED_AUDIO_FORMATS = SUPPORTED_AUDIO_FORMATS
|
|
119
|
-
module.exports.FFmpegDecoder = FFmpegDecoder
|
|
413
|
+
module.exports = { FFmpegDecoder }
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/decoder-audio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Tether",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"main": "index.js",
|
|
9
|
-
"addon": true,
|
|
10
9
|
"scripts": {
|
|
11
10
|
"test:dts": "tsc index.d.ts --noEmit --esModuleInterop --skipLibCheck",
|
|
12
11
|
"test:unit": "brittle-bare test/unit/*.test.js",
|
|
@@ -18,14 +17,10 @@
|
|
|
18
17
|
"test:integration": "brittle-bare test/integration/*.test.js"
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"binding.js",
|
|
22
20
|
"index.js",
|
|
23
|
-
"gstreamer.js",
|
|
24
21
|
"constants.js",
|
|
25
22
|
"constants.d.ts",
|
|
26
|
-
"prebuilds",
|
|
27
23
|
"utils",
|
|
28
|
-
"lib",
|
|
29
24
|
"index.d.ts",
|
|
30
25
|
"test/mobile"
|
|
31
26
|
],
|
|
@@ -38,8 +33,6 @@
|
|
|
38
33
|
"devDependencies": {
|
|
39
34
|
"@types/node": "^22.14.1",
|
|
40
35
|
"brittle": "^3.13.1",
|
|
41
|
-
"cmake-bare": "^1.7.5",
|
|
42
|
-
"cmake-vcpkg": "^1.1.0",
|
|
43
36
|
"istanbul": "^0.4.5",
|
|
44
37
|
"standard": "^17.1.2",
|
|
45
38
|
"typescript": "^5.3.0"
|
|
Binary file
|
package/utils/error.js
CHANGED
|
@@ -40,7 +40,7 @@ addCodes({
|
|
|
40
40
|
},
|
|
41
41
|
[ERR_CODES.FAILED_TO_GET_STATUS]: {
|
|
42
42
|
name: 'FAILED_TO_GET_STATUS',
|
|
43
|
-
message: (message) => `Failed to get
|
|
43
|
+
message: (message) => `Failed to get decoder status, error: ${message}`
|
|
44
44
|
},
|
|
45
45
|
[ERR_CODES.FAILED_TO_DESTROY]: {
|
|
46
46
|
name: 'FAILED_TO_DESTROY',
|
package/binding.js
DELETED
package/gstreamer.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const binding = require('./binding')
|
|
4
|
-
const { QvacErrorDecoderAudio, ERR_CODES } = require('./utils/error')
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* An interface between Bare addon in C++ and JS runtime.
|
|
8
|
-
*/
|
|
9
|
-
class GSTDecoderInterface {
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {Object} configurationParams - all the required configuration for inference setup
|
|
13
|
-
* @param {Function} outputCb - to be called on any inference event ( started, new output, error, etc )
|
|
14
|
-
* @param {Function} transitionCb - to be called on addon state changes (LISTENING, IDLE, STOPPED, etc )
|
|
15
|
-
*/
|
|
16
|
-
constructor (configurationParams, outputCb, transitionCb = null) {
|
|
17
|
-
this._handle = binding.createInstance(this, configurationParams, outputCb, transitionCb)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
* @param {Object} weightsData
|
|
23
|
-
* @param {String} weightsData.filename
|
|
24
|
-
* @param {Uint8Array} weightsData.contents
|
|
25
|
-
* @param {Boolean} weightsData.completed
|
|
26
|
-
*/
|
|
27
|
-
async loadWeights (weightsData) {
|
|
28
|
-
try {
|
|
29
|
-
binding.loadWeights(this._handle, weightsData)
|
|
30
|
-
} catch (err) {
|
|
31
|
-
throw new QvacErrorDecoderAudio(
|
|
32
|
-
ERR_CODES.FAILED_TO_LOAD_WEIGHTS,
|
|
33
|
-
err.message
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Moves addon to the LISTENING state after all the initialization is done
|
|
40
|
-
*/
|
|
41
|
-
async activate () {
|
|
42
|
-
try {
|
|
43
|
-
binding.activate(this._handle)
|
|
44
|
-
} catch (err) {
|
|
45
|
-
throw new QvacErrorDecoderAudio(
|
|
46
|
-
ERR_CODES.FAILED_TO_ACTIVATE,
|
|
47
|
-
err.message
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Pauses current inference process
|
|
54
|
-
*/
|
|
55
|
-
async pause () {
|
|
56
|
-
try {
|
|
57
|
-
binding.pause(this._handle)
|
|
58
|
-
} catch (err) {
|
|
59
|
-
throw new QvacErrorDecoderAudio(
|
|
60
|
-
ERR_CODES.FAILED_TO_PAUSE,
|
|
61
|
-
err.message
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Cancel a inference process by jobId, if no jobId is provided it cancel the whole queue
|
|
68
|
-
*/
|
|
69
|
-
async cancel (jobId) {
|
|
70
|
-
try {
|
|
71
|
-
binding.cancel(this._handle, jobId)
|
|
72
|
-
} catch (err) {
|
|
73
|
-
throw new QvacErrorDecoderAudio(
|
|
74
|
-
ERR_CODES.FAILED_TO_CANCEL,
|
|
75
|
-
err.message
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Adds new input to the processing queue
|
|
82
|
-
* @param {Object} data
|
|
83
|
-
* @param {String} data.type
|
|
84
|
-
* @param {String} data.input
|
|
85
|
-
* @returns {Number} - job ID
|
|
86
|
-
*/
|
|
87
|
-
async append (data) {
|
|
88
|
-
try {
|
|
89
|
-
return binding.append(this._handle, data)
|
|
90
|
-
} catch (err) {
|
|
91
|
-
throw new QvacErrorDecoderAudio(
|
|
92
|
-
ERR_CODES.FAILED_TO_APPEND,
|
|
93
|
-
err.message
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Addon process status
|
|
100
|
-
* @returns {String}
|
|
101
|
-
*/
|
|
102
|
-
async status () {
|
|
103
|
-
try {
|
|
104
|
-
return binding.status(this._handle)
|
|
105
|
-
} catch (err) {
|
|
106
|
-
throw new QvacErrorDecoderAudio(
|
|
107
|
-
ERR_CODES.FAILED_TO_GET_STATUS,
|
|
108
|
-
err.message
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Stops addon process and clears resources (including memory).
|
|
115
|
-
*/
|
|
116
|
-
async destroy () {
|
|
117
|
-
try {
|
|
118
|
-
binding.destroyInstance(this._handle)
|
|
119
|
-
this._handle = null
|
|
120
|
-
} catch (err) {
|
|
121
|
-
throw new QvacErrorDecoderAudio(
|
|
122
|
-
ERR_CODES.FAILED_TO_DESTROY,
|
|
123
|
-
err.message
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
module.exports = {
|
|
130
|
-
GSTDecoderInterface
|
|
131
|
-
}
|