@qvac/decoder-audio 0.3.1 → 0.3.2

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/index.d.ts CHANGED
@@ -33,6 +33,17 @@ interface DecoderStatus {
33
33
  paused: boolean
34
34
  }
35
35
 
36
+ interface RuntimeStats {
37
+ decodeTimeMs: number
38
+ inputBytes: number
39
+ outputBytes: number
40
+ samplesDecoded: number
41
+ codecName: string | null
42
+ inputSampleRate: number
43
+ outputSampleRate: number
44
+ audioFormat: 's16le' | 'f32le'
45
+ }
46
+
36
47
  declare class FFmpegDecoder extends BaseInference {
37
48
  SUPPORTED_AUDIO_FORMATS: SupportedAudioFormats
38
49
  OUTPUT_CHANNEL_LAYOUT: number | null
@@ -46,6 +57,8 @@ declare class FFmpegDecoder extends BaseInference {
46
57
  unpause(): Promise<void>
47
58
  stop(): Promise<void>
48
59
  status(): DecoderStatus
60
+
61
+ runtimeStats(): RuntimeStats
49
62
  }
50
63
 
51
- export { FFmpegDecoder }
64
+ export { FFmpegDecoder, RuntimeStats }
package/index.js CHANGED
@@ -59,6 +59,33 @@ class FFmpegDecoder extends BaseInference {
59
59
  // Encoder delay handling
60
60
  this.samplesSkipped = 0
61
61
  this.totalSkipSamples = 0
62
+
63
+ // Runtime stats
64
+ this._resetStats()
65
+ }
66
+
67
+ /**
68
+ * Resets the runtime stats
69
+ */
70
+ _resetStats () {
71
+ this._runtimeStats = {
72
+ decodeTimeMs: 0,
73
+ inputBytes: 0,
74
+ outputBytes: 0,
75
+ samplesDecoded: 0,
76
+ codecName: null,
77
+ inputSampleRate: 0,
78
+ outputSampleRate: this.config.sampleRate,
79
+ audioFormat: this.config.audioFormat
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get the current runtime stats
85
+ * @returns {Object} Current runtime stats
86
+ */
87
+ runtimeStats () {
88
+ return { ...this._runtimeStats }
62
89
  }
63
90
 
64
91
  /**
@@ -172,10 +199,18 @@ class FFmpegDecoder extends BaseInference {
172
199
  const length = OUTPUT_FORMAT_BYTE_LENGTH * (count - samplesToSkip) * output.channelLayout.nbChannels
173
200
  const chunk = Buffer.from(samples.data.subarray(skipBytes, skipBytes + length))
174
201
  job.response.updateOutput({ outputArray: chunk })
202
+
203
+ // Track stats for partial frame
204
+ this._runtimeStats.samplesDecoded += (count - samplesToSkip)
205
+ this._runtimeStats.outputBytes += length
175
206
  } else {
176
207
  const length = OUTPUT_FORMAT_BYTE_LENGTH * count * output.channelLayout.nbChannels
177
208
  const chunk = Buffer.from(samples.data.subarray(0, length))
178
209
  job.response.updateOutput({ outputArray: chunk })
210
+
211
+ // Track stats
212
+ this._runtimeStats.samplesDecoded += count
213
+ this._runtimeStats.outputBytes += length
179
214
  }
180
215
  }
181
216
  }
@@ -195,6 +230,10 @@ class FFmpegDecoder extends BaseInference {
195
230
 
196
231
  this.logger.debug('[FFmpegDecoder] Stream codec:', stream.codec, stream.codecParameters)
197
232
 
233
+ // Track codec info in stats
234
+ this._runtimeStats.codecName = stream.codec.name
235
+ this._runtimeStats.inputSampleRate = stream.codecParameters.sampleRate
236
+
198
237
  const packet = new ffmpeg.Packet()
199
238
  const raw = new ffmpeg.Frame()
200
239
 
@@ -248,6 +287,10 @@ class FFmpegDecoder extends BaseInference {
248
287
  const actualLength = OUTPUT_FORMAT_BYTE_LENGTH * flushCount * output.channelLayout.nbChannels
249
288
  const chunk = Buffer.from(samples.data.subarray(0, actualLength))
250
289
  job.response.updateOutput({ outputArray: chunk })
290
+
291
+ // Track stats for flushed samples
292
+ this._runtimeStats.samplesDecoded += flushCount
293
+ this._runtimeStats.outputBytes += actualLength
251
294
  }
252
295
 
253
296
  decoder.destroy()
@@ -282,6 +325,10 @@ class FFmpegDecoder extends BaseInference {
282
325
  return
283
326
  }
284
327
 
328
+ // Reset and start tracking stats
329
+ this._resetStats()
330
+ const startTime = Date.now()
331
+
285
332
  try {
286
333
  this.logger.info('[FFmpegDecoder] Starting stream processing')
287
334
 
@@ -289,6 +336,9 @@ class FFmpegDecoder extends BaseInference {
289
336
  const audioBuffer = await this._collectStreamData(audioStream, job)
290
337
  this.logger.info(`[FFmpegDecoder] Collected ${audioBuffer.length} bytes of audio data`)
291
338
 
339
+ // Track input bytes
340
+ this._runtimeStats.inputBytes = audioBuffer.length
341
+
292
342
  if (!job.isActive) {
293
343
  this.logger.info('[FFmpegDecoder] Job cancelled after data collection')
294
344
  return
@@ -353,10 +403,21 @@ class FFmpegDecoder extends BaseInference {
353
403
  // Process the stream and generate decoded output
354
404
  this._processFFmpegStream(format, format.streams[streamIndex], job)
355
405
 
406
+ // Calculate final decode time
407
+ this._runtimeStats.decodeTimeMs = Date.now() - startTime
408
+
409
+ // Update stats on response before ending
410
+ job.response.updateStats(this.runtimeStats())
411
+
356
412
  // Mark as complete
357
413
  job.response.ended()
358
414
  this.logger.info('[FFmpegDecoder] Stream processing completed successfully')
415
+ this.logger.info(`[FFmpegDecoder] Runtime stats: ${JSON.stringify(this._runtimeStats)}`)
359
416
  } catch (err) {
417
+ // Still capture stats even on error
418
+ this._runtimeStats.decodeTimeMs = Date.now() - startTime
419
+ job.response.updateStats(this.runtimeStats())
420
+
360
421
  this.logger.error('Error processing audio stream:', err)
361
422
  job.response.failed(err)
362
423
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/decoder-audio",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Tether",
@@ -12,9 +12,12 @@
12
12
  "test": "npm run test:unit && npm run test:integration",
13
13
  "coverage:unit": "brittle-bare --coverage --cov-dir=coverage/unit test/unit/*.test.js && npx istanbul report html --include=coverage/unit/coverage-final.json",
14
14
  "coverage": "brittle-bare --coverage --cov-dir=coverage/unit test/unit/*.test.js && npx istanbul report html --include=coverage/unit/coverage-final.json",
15
- "lint": "standard \"test/**/*.js\" \"*.js\"",
16
- "lint:fix": "standard --fix \"test/**/*.js\" \"*.js\"",
17
- "test:integration": "brittle-bare test/integration/*.test.js"
15
+ "lint": "standard \"test/**/*.js\" \"*.js\" \"scripts/**/*.js\"",
16
+ "lint:fix": "standard --fix \"test/**/*.js\" \"*.js\" \"scripts/**/*.js\"",
17
+ "test:integration": "npm run test:integration:generate && brittle-bare test/integration/*.test.js",
18
+ "test:integration:generate": "brittle -r test/integration/all.js test/integration/*.test.js && npm run test:mobile:generate",
19
+ "test:mobile:generate": "bare ./scripts/generate-mobile-integration-tests.js",
20
+ "test:mobile:validate": "node scripts/validate-mobile-tests.js"
18
21
  },
19
22
  "files": [
20
23
  "index.js",
@@ -32,6 +35,8 @@
32
35
  },
33
36
  "devDependencies": {
34
37
  "@types/node": "^22.14.1",
38
+ "bare-os": "^3.6.2",
39
+ "bare-url": "^2.1.6",
35
40
  "brittle": "^3.13.1",
36
41
  "istanbul": "^0.4.5",
37
42
  "standard": "^17.1.2",
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const path = require('bare-path')
4
+ const fs = require('bare-fs')
5
+ const { pathToFileURL } = require('bare-url')
6
+
7
+ async function runIntegrationModule (relativeModulePath, options = {}) {
8
+ const modulePath = path.join(__dirname, relativeModulePath)
9
+
10
+ if (!fs.existsSync(modulePath)) {
11
+ console.warn(`[integration-runner] Missing module: ${relativeModulePath}`)
12
+ return 'missing'
13
+ }
14
+
15
+ const moduleUrl = pathToFileURL(modulePath).href
16
+ await import(moduleUrl)
17
+ return modulePath
18
+ }
19
+
20
+ global.runIntegrationModule = runIntegrationModule
21
+
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+ require('./integration-runtime.cjs')
3
+
4
+ // AUTO-GENERATED FILE. Run `npm run test:mobile:generate` to update.
5
+ // Each function mirrors a single file under test/integration/.
6
+
7
+ /* global runIntegrationModule */
8
+
9
+ async function runFfmpegDecoderTest (options = {}) { // eslint-disable-line no-unused-vars
10
+ return runIntegrationModule('../integration/ffmpeg-decoder.test.js', options)
11
+ }
@@ -1,105 +0,0 @@
1
- const { FFmpegDecoder } = require('@qvac/decoder-audio')
2
-
3
- async function testFFmpegDecodeMp3() {
4
- try {
5
- const decoder = new FFmpegDecoder({
6
- config: {
7
- audioFormat: 's16le',
8
- sampleRate: 16000
9
- }
10
- })
11
-
12
- await decoder.load()
13
-
14
- const audioPath = getAssetPath('sample.mp3')
15
- const audioStream = fs.createReadStream(audioPath)
16
-
17
- const response = await decoder.run(audioStream)
18
-
19
- let totalBytes = 0
20
- let updateCount = 0
21
-
22
- await response
23
- .onUpdate(output => {
24
- if (output && output.outputArray) {
25
- const bytes = new Uint8Array(output.outputArray)
26
- totalBytes += bytes.length
27
- updateCount++
28
- }
29
- })
30
- .await()
31
-
32
- if (totalBytes === 0) {
33
- throw new Error('No audio data decoded')
34
- }
35
-
36
- if (updateCount === 0) {
37
- throw new Error('No decoder updates received')
38
- }
39
-
40
- console.log(`MP3 decode complete: ${totalBytes} bytes, ${updateCount} updates`)
41
-
42
- await decoder.unload()
43
-
44
- return {
45
- success: true,
46
- totalBytes,
47
- updateCount
48
- }
49
- } catch (error) {
50
- console.error('Error during testFFmpegDecodeMp3:', error)
51
- throw error
52
- }
53
- }
54
-
55
- async function testFFmpegDecodeWav() {
56
- try {
57
- const decoder = new FFmpegDecoder({
58
- config: {
59
- audioFormat: 's16le',
60
- sampleRate: 16000
61
- }
62
- })
63
-
64
- await decoder.load()
65
-
66
- const audioPath = getAssetPath('sample.wav')
67
- const audioStream = fs.createReadStream(audioPath)
68
-
69
- const response = await decoder.run(audioStream)
70
-
71
- let totalBytes = 0
72
- let updateCount = 0
73
-
74
- await response
75
- .onUpdate(output => {
76
- if (output && output.outputArray) {
77
- const bytes = new Uint8Array(output.outputArray)
78
- totalBytes += bytes.length
79
- updateCount++
80
- }
81
- })
82
- .await()
83
-
84
- if (totalBytes === 0) {
85
- throw new Error('No audio data decoded')
86
- }
87
-
88
- if (updateCount === 0) {
89
- throw new Error('No decoder updates received')
90
- }
91
-
92
- console.log(`MP3 decode complete: ${totalBytes} bytes, ${updateCount} updates`)
93
-
94
- await decoder.unload()
95
-
96
- return {
97
- success: true,
98
- totalBytes,
99
- updateCount
100
- }
101
- } catch (error) {
102
- console.error('Error during testFFmpegDecodeMp3:', error)
103
- throw error
104
- }
105
- }