@qvac/decoder-audio 0.3.8 → 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/README.md +2 -2
- package/index.d.ts +5 -17
- package/index.js +106 -175
- package/package.json +4 -6
- package/utils/error.js +6 -1
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,15 +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')
|
|
7
6
|
const { QvacErrorDecoderAudio, ERR_CODES } = require('./utils/error')
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* FFmpeg-based audio decoder (single-threaded)
|
|
11
10
|
*/
|
|
12
|
-
class FFmpegDecoder
|
|
11
|
+
class FFmpegDecoder {
|
|
13
12
|
SUPPORTED_AUDIO_FORMATS = {
|
|
14
13
|
s16le: {
|
|
15
14
|
format: null, // Will be set to ffmpeg.constants.sampleFormats.S16
|
|
@@ -29,7 +28,6 @@ class FFmpegDecoder extends BaseInference {
|
|
|
29
28
|
* @param streamIndex - Index of the stream to decode. Default: 0
|
|
30
29
|
* @param inputBitrate - Input audio bitrate. Default: 192000
|
|
31
30
|
* @param audioFormat - Output audio format. Default: 's16le'
|
|
32
|
-
* @param args - Additional arguments passed to BaseInference
|
|
33
31
|
* @param {Object} [config.streamIndex] - Index of the stream to decode (default: 0)
|
|
34
32
|
* @param {number} [config.inputBitrate] - Input audio bitrate (default: 192000)
|
|
35
33
|
* @param {string} [config.audioFormat] - Output audio format (default: 'f32le')
|
|
@@ -41,11 +39,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
41
39
|
logger = null,
|
|
42
40
|
streamIndex = 0,
|
|
43
41
|
inputBitrate = 192000,
|
|
44
|
-
audioFormat = 's16le'
|
|
45
|
-
|
|
46
|
-
}) {
|
|
47
|
-
super({ ...args, logger })
|
|
48
|
-
|
|
42
|
+
audioFormat = 's16le'
|
|
43
|
+
} = {}) {
|
|
49
44
|
this.config = {
|
|
50
45
|
streamIndex: config.streamIndex || streamIndex,
|
|
51
46
|
inputBitrate: config.inputBitrate || inputBitrate,
|
|
@@ -55,7 +50,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
55
50
|
|
|
56
51
|
this.logger = new QvacLogger(logger)
|
|
57
52
|
this.isLoaded = false
|
|
58
|
-
this.
|
|
53
|
+
this._cancelled = false
|
|
54
|
+
this._job = createJobHandler({ cancel: () => this._cancelCurrent() })
|
|
59
55
|
|
|
60
56
|
// Encoder delay handling
|
|
61
57
|
this.samplesSkipped = 0
|
|
@@ -128,7 +124,8 @@ class FFmpegDecoder extends BaseInference {
|
|
|
128
124
|
this.logger.info('Unloading FFmpegDecoder')
|
|
129
125
|
|
|
130
126
|
this.isLoaded = false
|
|
131
|
-
this.
|
|
127
|
+
this._cancelCurrent()
|
|
128
|
+
this._job.fail(new QvacErrorDecoderAudio({ code: ERR_CODES.DECODER_NOT_LOADED }))
|
|
132
129
|
this.logger.info('FFmpegDecoder unloaded')
|
|
133
130
|
}
|
|
134
131
|
|
|
@@ -137,41 +134,41 @@ class FFmpegDecoder extends BaseInference {
|
|
|
137
134
|
* @param {Readable} audioStream - Input audio stream
|
|
138
135
|
* @returns {QvacResponse} Response with decoded audio
|
|
139
136
|
*/
|
|
140
|
-
|
|
137
|
+
run (audioStream) {
|
|
141
138
|
if (!this.isLoaded) {
|
|
142
139
|
throw new QvacErrorDecoderAudio({ code: ERR_CODES.DECODER_NOT_LOADED })
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
this.logger.info('Starting new audio stream processing')
|
|
146
143
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
pauseHandler: () => this.pause(),
|
|
150
|
-
continueHandler: () => this.unpause()
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
this.currentJob = {
|
|
154
|
-
response,
|
|
155
|
-
audioChunks: [],
|
|
156
|
-
isActive: true,
|
|
157
|
-
isPaused: false
|
|
158
|
-
}
|
|
144
|
+
this._cancelled = false
|
|
145
|
+
const response = this._job.start()
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
})
|
|
165
156
|
|
|
166
157
|
return response
|
|
167
158
|
}
|
|
168
159
|
|
|
160
|
+
_cancelCurrent () {
|
|
161
|
+
this._cancelled = true
|
|
162
|
+
this.logger.debug('Decoder cancel requested')
|
|
163
|
+
return Promise.resolve()
|
|
164
|
+
}
|
|
165
|
+
|
|
169
166
|
_getBufferSize (inputBitrate) {
|
|
170
167
|
const maxBufferSize = 1024 * 1024 // 1MB max
|
|
171
168
|
return Math.min((inputBitrate / 8) * 4, maxBufferSize)
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
_processFrame (decoder, raw, resampler
|
|
171
|
+
_processFrame (decoder, raw, resampler) {
|
|
175
172
|
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
176
173
|
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
177
174
|
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
@@ -202,7 +199,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
202
199
|
const skipBytes = OUTPUT_FORMAT_BYTE_LENGTH * samplesToSkip * output.channelLayout.nbChannels
|
|
203
200
|
const length = OUTPUT_FORMAT_BYTE_LENGTH * (count - samplesToSkip) * output.channelLayout.nbChannels
|
|
204
201
|
const chunk = Buffer.from(samples.data.subarray(skipBytes, skipBytes + length))
|
|
205
|
-
|
|
202
|
+
this._job.output({ outputArray: chunk })
|
|
206
203
|
|
|
207
204
|
// Track stats for partial frame
|
|
208
205
|
this._runtimeStats.samplesDecoded += (count - samplesToSkip)
|
|
@@ -210,7 +207,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
210
207
|
} else {
|
|
211
208
|
const length = OUTPUT_FORMAT_BYTE_LENGTH * count * output.channelLayout.nbChannels
|
|
212
209
|
const chunk = Buffer.from(samples.data.subarray(0, length))
|
|
213
|
-
|
|
210
|
+
this._job.output({ outputArray: chunk })
|
|
214
211
|
|
|
215
212
|
// Track stats
|
|
216
213
|
this._runtimeStats.samplesDecoded += count
|
|
@@ -219,15 +216,19 @@ class FFmpegDecoder extends BaseInference {
|
|
|
219
216
|
}
|
|
220
217
|
}
|
|
221
218
|
|
|
222
|
-
_processPacket (format, packet, raw, decoder, resampler
|
|
219
|
+
_processPacket (format, packet, raw, decoder, resampler) {
|
|
223
220
|
while (format.readFrame(packet)) {
|
|
221
|
+
if (this._cancelled) {
|
|
222
|
+
packet.unref()
|
|
223
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.JOB_CANCELLED })
|
|
224
|
+
}
|
|
224
225
|
decoder.sendPacket(packet)
|
|
225
|
-
this._processFrame(decoder, raw, resampler
|
|
226
|
+
this._processFrame(decoder, raw, resampler)
|
|
226
227
|
packet.unref()
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
_processFFmpegStream (format, stream
|
|
231
|
+
_processFFmpegStream (format, stream) {
|
|
231
232
|
const OUTPUT_FORMAT = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].format
|
|
232
233
|
const OUTPUT_FORMAT_BYTE_LENGTH = this.SUPPORTED_AUDIO_FORMATS[this.config.audioFormat].byteLength
|
|
233
234
|
const OUTPUT_SAMPLE_RATE = this.config.sampleRate
|
|
@@ -270,7 +271,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
270
271
|
this.logger.info(`[FFmpegDecoder] Skipping ${skipMs}ms (${this.totalSkipSamples} samples) for ${codecName} to remove encoder artifacts`)
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
this._processPacket(format, packet, raw, decoder, resampler
|
|
274
|
+
this._processPacket(format, packet, raw, decoder, resampler)
|
|
274
275
|
|
|
275
276
|
// Flush resampler
|
|
276
277
|
const output = new ffmpeg.Frame()
|
|
@@ -290,7 +291,7 @@ class FFmpegDecoder extends BaseInference {
|
|
|
290
291
|
while ((flushCount = resampler.flush(output)) > 0) {
|
|
291
292
|
const actualLength = OUTPUT_FORMAT_BYTE_LENGTH * flushCount * output.channelLayout.nbChannels
|
|
292
293
|
const chunk = Buffer.from(samples.data.subarray(0, actualLength))
|
|
293
|
-
|
|
294
|
+
this._job.output({ outputArray: chunk })
|
|
294
295
|
|
|
295
296
|
// Track stats for flushed samples
|
|
296
297
|
this._runtimeStats.samplesDecoded += flushCount
|
|
@@ -300,19 +301,14 @@ class FFmpegDecoder extends BaseInference {
|
|
|
300
301
|
decoder.destroy()
|
|
301
302
|
}
|
|
302
303
|
|
|
303
|
-
async _collectStreamData (audioStream
|
|
304
|
+
async _collectStreamData (audioStream) {
|
|
304
305
|
const chunks = []
|
|
305
306
|
let totalBytes = 0
|
|
306
307
|
|
|
307
308
|
for await (const chunk of audioStream) {
|
|
308
|
-
if (
|
|
309
|
+
if (this._cancelled) {
|
|
309
310
|
this.logger.info('[FFmpegDecoder] Job cancelled, stopping stream collection')
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
while (job.isPaused) {
|
|
314
|
-
this.logger.debug('[FFmpegDecoder] Job is paused, waiting to resume...')
|
|
315
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
311
|
+
throw new QvacErrorDecoderAudio({ code: ERR_CODES.JOB_CANCELLED })
|
|
316
312
|
}
|
|
317
313
|
|
|
318
314
|
chunks.push(chunk)
|
|
@@ -324,157 +320,92 @@ class FFmpegDecoder extends BaseInference {
|
|
|
324
320
|
}
|
|
325
321
|
|
|
326
322
|
async _processStream (audioStream) {
|
|
327
|
-
const job = this.currentJob
|
|
328
|
-
if (!job.isActive) {
|
|
329
|
-
return
|
|
330
|
-
}
|
|
331
|
-
|
|
332
323
|
// Reset and start tracking stats
|
|
333
324
|
this._resetStats()
|
|
334
325
|
const startTime = Date.now()
|
|
335
326
|
|
|
336
|
-
|
|
337
|
-
this.logger.info('[FFmpegDecoder] Starting stream processing')
|
|
327
|
+
this.logger.info('[FFmpegDecoder] Starting stream processing')
|
|
338
328
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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`)
|
|
342
332
|
|
|
343
|
-
|
|
344
|
-
|
|
333
|
+
// Track input bytes
|
|
334
|
+
this._runtimeStats.inputBytes = audioBuffer.length
|
|
345
335
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
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
|
+
}
|
|
350
341
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const io = new ffmpeg.IOContext(bufferSize, {
|
|
356
|
-
onread: (buffer, requestedLen) => {
|
|
357
|
-
const remainingBytes = audioBuffer.length - bufferOffset
|
|
358
|
-
const bytesToRead = Math.min(requestedLen, remainingBytes)
|
|
359
|
-
|
|
360
|
-
if (bytesToRead <= 0) {
|
|
361
|
-
return 0 // EOF
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
|
|
365
|
-
bufferOffset += bytesToRead
|
|
366
|
-
|
|
367
|
-
this.logger.debug(`[FFmpegDecoder] Read ${bytesToRead} bytes from buffer, offset now: ${bufferOffset}`)
|
|
368
|
-
return bytesToRead
|
|
369
|
-
},
|
|
370
|
-
onseek: (offset, whence) => {
|
|
371
|
-
const AVSEEK_SIZE = 0x10000
|
|
372
|
-
|
|
373
|
-
if (whence === AVSEEK_SIZE) {
|
|
374
|
-
return audioBuffer.length
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
let newOffset
|
|
378
|
-
if (whence === 0) {
|
|
379
|
-
newOffset = offset
|
|
380
|
-
} else if (whence === 1) {
|
|
381
|
-
newOffset = bufferOffset + offset
|
|
382
|
-
} else if (whence === 2) {
|
|
383
|
-
newOffset = audioBuffer.length + offset
|
|
384
|
-
} else {
|
|
385
|
-
return -1
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (newOffset < 0 || newOffset > audioBuffer.length) {
|
|
389
|
-
return -1
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
bufferOffset = newOffset
|
|
393
|
-
this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
|
|
394
|
-
return bufferOffset
|
|
395
|
-
}
|
|
396
|
-
})
|
|
342
|
+
// Create FFmpeg IO context with the buffer
|
|
343
|
+
const bufferSize = this._getBufferSize(this.config.inputBitrate)
|
|
344
|
+
let bufferOffset = 0
|
|
397
345
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
346
|
+
const io = new ffmpeg.IOContext(bufferSize, {
|
|
347
|
+
onread: (buffer, requestedLen) => {
|
|
348
|
+
const remainingBytes = audioBuffer.length - bufferOffset
|
|
349
|
+
const bytesToRead = Math.min(requestedLen, remainingBytes)
|
|
401
350
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
code: ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS,
|
|
406
|
-
adds: streamIndex
|
|
407
|
-
})
|
|
408
|
-
}
|
|
351
|
+
if (bytesToRead <= 0) {
|
|
352
|
+
return 0 // EOF
|
|
353
|
+
}
|
|
409
354
|
|
|
410
|
-
|
|
411
|
-
|
|
355
|
+
audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
|
|
356
|
+
bufferOffset += bytesToRead
|
|
412
357
|
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
415
363
|
|
|
416
|
-
|
|
417
|
-
|
|
364
|
+
if (whence === AVSEEK_SIZE) {
|
|
365
|
+
return audioBuffer.length
|
|
366
|
+
}
|
|
418
367
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
+
}
|
|
427
378
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
379
|
+
if (newOffset < 0 || newOffset > audioBuffer.length) {
|
|
380
|
+
return -1
|
|
381
|
+
}
|
|
431
382
|
|
|
432
|
-
|
|
433
|
-
|
|
383
|
+
bufferOffset = newOffset
|
|
384
|
+
this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
|
|
385
|
+
return bufferOffset
|
|
386
|
+
}
|
|
387
|
+
})
|
|
434
388
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
pause () {
|
|
439
|
-
if (this.currentJob) {
|
|
440
|
-
this.currentJob.isPaused = true
|
|
441
|
-
this.logger.debug('Decoder paused')
|
|
442
|
-
}
|
|
443
|
-
return Promise.resolve()
|
|
444
|
-
}
|
|
389
|
+
this.logger.debug('[FFmpegDecoder] IOContext created')
|
|
390
|
+
const format = new ffmpeg.InputFormatContext(io)
|
|
391
|
+
this.logger.debug('[FFmpegDecoder] InputFormatContext created')
|
|
445
392
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.logger.debug('Decoder unpaused')
|
|
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
|
+
})
|
|
453
399
|
}
|
|
454
|
-
return Promise.resolve()
|
|
455
|
-
}
|
|
456
400
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
*/
|
|
460
|
-
stop () {
|
|
461
|
-
if (this.currentJob) {
|
|
462
|
-
this.currentJob.isActive = false
|
|
463
|
-
this.currentJob.response.finish()
|
|
464
|
-
this.logger.debug('Decoder stopped')
|
|
465
|
-
}
|
|
466
|
-
return Promise.resolve()
|
|
467
|
-
}
|
|
401
|
+
// Process the stream and generate decoded output
|
|
402
|
+
this._processFFmpegStream(format, format.streams[streamIndex])
|
|
468
403
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
loaded: this.isLoaded,
|
|
475
|
-
active: this.currentJob?.isActive || false,
|
|
476
|
-
paused: this.currentJob?.isPaused || false
|
|
477
|
-
}
|
|
404
|
+
// Calculate final decode time
|
|
405
|
+
this._runtimeStats.decodeTimeMs = Date.now() - startTime
|
|
406
|
+
|
|
407
|
+
this.logger.info('[FFmpegDecoder] Stream processing completed successfully')
|
|
408
|
+
this.logger.info(`[FFmpegDecoder] Runtime stats: ${JSON.stringify(this._runtimeStats)}`)
|
|
478
409
|
}
|
|
479
410
|
}
|
|
480
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",
|
|
@@ -48,21 +48,19 @@
|
|
|
48
48
|
"@qvac/error": "^0.1.0",
|
|
49
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
|
}
|
package/utils/error.js
CHANGED
|
@@ -17,7 +17,8 @@ const ERR_CODES = Object.freeze({
|
|
|
17
17
|
BUFFER_SIZE_TOO_SMALL: 11008,
|
|
18
18
|
UNSUPPORTED_AUDIO_FORMAT: 11009,
|
|
19
19
|
DECODER_NOT_LOADED: 11010,
|
|
20
|
-
STREAM_INDEX_OUT_OF_BOUNDS: 11011
|
|
20
|
+
STREAM_INDEX_OUT_OF_BOUNDS: 11011,
|
|
21
|
+
JOB_CANCELLED: 11012
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
addCodes({
|
|
@@ -64,6 +65,10 @@ addCodes({
|
|
|
64
65
|
[ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS]: {
|
|
65
66
|
name: 'STREAM_INDEX_OUT_OF_BOUNDS',
|
|
66
67
|
message: (index) => `Stream index out of bounds: ${index}`
|
|
68
|
+
},
|
|
69
|
+
[ERR_CODES.JOB_CANCELLED]: {
|
|
70
|
+
name: 'JOB_CANCELLED',
|
|
71
|
+
message: 'Decoder job cancelled'
|
|
67
72
|
}
|
|
68
73
|
}, {
|
|
69
74
|
name,
|