@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 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.7.5
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.1.0
48
+ bare-ffmpeg@1.2.2
42
49
  https://github.com/holepunchto/bare-ffmpeg
43
- bare-fs@4.5.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.6.2
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.3
62
+ bare-pipe@4.1.5
56
63
  https://github.com/holepunchto/bare-pipe
57
- bare-process@4.2.2
64
+ bare-process@4.4.1
58
65
  https://github.com/holepunchto/bare-process
59
- bare-semver@1.0.2
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-stream@2.8.0
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.2
74
+ bare-structured-clone@1.5.4
66
75
  https://github.com/holepunchto/bare-structured-clone
67
- bare-tty@5.0.3
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.3.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.18.0
76
- https://github.com/compact-encoding/compact-encoding
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.4.5
92
+ fs-native-extensions@1.5.0
84
93
  https://github.com/holepunchto/fs-native-extensions
85
- hyperblobs@2.9.0
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.4.1
92
- hyperdrive@13.2.1
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.19.1
106
+ hyperschema@1.20.1
95
107
  https://github.com/holepunchto/hyperschema
96
- index-encoder@3.4.0
108
+ index-encoder@3.5.0
97
109
  https://github.com/holepunchto/index-encoder
98
- mirror-drive@1.12.0
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.13.0
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.8.0
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.1
161
- https://github.com/mafintosh/dht-rpc
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.24.0
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.29.0
192
+ hyperdht@6.30.0
177
193
  https://github.com/holepunchto/hyperdht
178
- hyperswarm@4.16.0
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.1
193
- https://github.com/mafintosh/protomux
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.2
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.10
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.23.0
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
- # qvac-lib-decoder-audio
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/qvac-lib-decoder-audio)
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 BaseInference = require('@qvac/infer-base/WeightsProvider/BaseInference')
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 extends BaseInference {
43
+ declare class FFmpegDecoder {
52
44
  SUPPORTED_AUDIO_FORMATS: SupportedAudioFormats
53
45
  OUTPUT_CHANNEL_LAYOUT: number | null
54
46
 
55
- constructor(params: FFmpegDecoderConstructorParams)
47
+ constructor(params?: FFmpegDecoderConstructorParams)
56
48
 
57
49
  load(): Promise<void>
58
50
  unload(): Promise<void>
59
- run(audioStream: AsyncIterable<Buffer>): Promise<QvacResponse<DecoderOutput>>
60
- pause(): Promise<void>
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 BaseInference = require('@qvac/infer-base/WeightsProvider/BaseInference')
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 extends BaseInference {
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
- ...args
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.currentJob = null
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 Error(`Unsupported audio format: ${this.config.audioFormat}`)
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.currentJob = null
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
- async run (audioStream) {
137
+ run (audioStream) {
137
138
  if (!this.isLoaded) {
138
- throw new Error('Decoder not loaded. Call load() first.')
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
- const response = new QvacResponse({
144
- cancelHandler: () => this.stop(),
145
- pauseHandler: () => this.pause(),
146
- continueHandler: () => this.unpause()
147
- })
144
+ this._cancelled = false
145
+ const response = this._job.start()
148
146
 
149
- this.currentJob = {
150
- response,
151
- audioChunks: [],
152
- isActive: true,
153
- isPaused: false
154
- }
155
-
156
- // Process the audio stream
157
- this._processStream(audioStream).catch(err => {
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, job) {
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
- job.response.updateOutput({ outputArray: chunk })
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
- job.response.updateOutput({ outputArray: chunk })
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, job) {
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, job)
226
+ this._processFrame(decoder, raw, resampler)
222
227
  packet.unref()
223
228
  }
224
229
  }
225
230
 
226
- _processFFmpegStream (format, stream, job) {
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, job)
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
- job.response.updateOutput({ outputArray: chunk })
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, job) {
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 (!job.isActive) {
309
+ if (this._cancelled) {
305
310
  this.logger.info('[FFmpegDecoder] Job cancelled, stopping stream collection')
306
- break
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
- try {
333
- this.logger.info('[FFmpegDecoder] Starting stream processing')
327
+ this.logger.info('[FFmpegDecoder] Starting stream processing')
334
328
 
335
- // Collect all audio data from stream
336
- const audioBuffer = await this._collectStreamData(audioStream, job)
337
- this.logger.info(`[FFmpegDecoder] Collected ${audioBuffer.length} bytes of audio data`)
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
- // Track input bytes
340
- this._runtimeStats.inputBytes = audioBuffer.length
333
+ // Track input bytes
334
+ this._runtimeStats.inputBytes = audioBuffer.length
341
335
 
342
- if (!job.isActive) {
343
- this.logger.info('[FFmpegDecoder] Job cancelled after data collection')
344
- return
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
- // Create FFmpeg IO context with the buffer
348
- const bufferSize = this._getBufferSize(this.config.inputBitrate)
349
- let bufferOffset = 0
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
- this.logger.debug('[FFmpegDecoder] IOContext created')
395
- const format = new ffmpeg.InputFormatContext(io)
396
- this.logger.debug('[FFmpegDecoder] InputFormatContext created')
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
- const streamIndex = this.config.streamIndex || 0
399
- if (format.streams[streamIndex] === undefined) {
400
- throw new Error('Stream index out of bounds')
401
- }
351
+ if (bytesToRead <= 0) {
352
+ return 0 // EOF
353
+ }
402
354
 
403
- // Process the stream and generate decoded output
404
- this._processFFmpegStream(format, format.streams[streamIndex], job)
355
+ audioBuffer.copy(buffer, 0, bufferOffset, bufferOffset + bytesToRead)
356
+ bufferOffset += bytesToRead
405
357
 
406
- // Calculate final decode time
407
- this._runtimeStats.decodeTimeMs = Date.now() - startTime
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
- // Update stats on response before ending
410
- job.response.updateStats(this.runtimeStats())
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
- // Mark as complete
413
- job.response.ended()
414
- this.logger.info('[FFmpegDecoder] Stream processing completed successfully')
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
- this.logger.error('Error processing audio stream:', err)
422
- job.response.failed(err)
423
- }
383
+ bufferOffset = newOffset
384
+ this.logger.debug(`[FFmpegDecoder] Seek to offset: ${bufferOffset}`)
385
+ return bufferOffset
386
+ }
387
+ })
424
388
 
425
- this.logger.info('Audio _processStream completed')
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
- * Pause the current job
430
- */
431
- pause () {
432
- if (this.currentJob) {
433
- this.currentJob.isPaused = true
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
- * Unpause the current job
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
- * Stop the current job
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
- * Get the current status
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.7",
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.1.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/qvac-lib-decoder-audio"
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/qvac-lib-decoder-audio#readme",
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,