@siteed/expo-audio-stream 1.7.2 → 1.8.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/CHANGELOG.md +17 -1
- package/README.md +6 -1
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +39 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +124 -12
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +26 -2
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +1 -0
- package/build/AudioRecorder.provider.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +35 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +14 -3
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +102 -38
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/WebRecorder.web.d.ts +11 -2
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +178 -43
- package/build/WebRecorder.web.js.map +1 -1
- package/build/events.d.ts +6 -0
- package/build/events.d.ts.map +1 -1
- package/build/events.js.map +1 -1
- package/build/useAudioRecorder.d.ts +3 -2
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +46 -5
- package/build/useAudioRecorder.js.map +1 -1
- package/ios/AudioStreamManager.swift +127 -8
- package/ios/AudioStreamManagerDelegate.swift +8 -2
- package/ios/ExpoAudioStreamModule.swift +61 -46
- package/ios/RecordingResult.swift +2 -0
- package/ios/RecordingSettings.swift +63 -3
- package/package.json +1 -1
- package/src/AudioRecorder.provider.tsx +1 -0
- package/src/ExpoAudioStream.types.ts +38 -1
- package/src/ExpoAudioStream.web.ts +114 -38
- package/src/WebRecorder.web.ts +210 -64
- package/src/events.ts +7 -0
- package/src/useAudioRecorder.tsx +70 -8
package/src/WebRecorder.web.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/WebRecorder.ts
|
|
2
2
|
|
|
3
3
|
import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
|
|
4
|
-
import { ConsoleLike, RecordingConfig } from './ExpoAudioStream.types'
|
|
4
|
+
import { ConsoleLike, RecordingConfig, WebRecordingOptions } from './ExpoAudioStream.types'
|
|
5
5
|
import {
|
|
6
6
|
EmitAudioAnalysisFunction,
|
|
7
7
|
EmitAudioEventFunction,
|
|
@@ -53,6 +53,11 @@ export class WebRecorder {
|
|
|
53
53
|
private audioAnalysisData: AudioAnalysis // Keep updating the full audio analysis data with latest events
|
|
54
54
|
private packetCount: number = 0
|
|
55
55
|
private logger?: ConsoleLike
|
|
56
|
+
private compressedMediaRecorder: MediaRecorder | null = null
|
|
57
|
+
private compressedChunks: Blob[] = []
|
|
58
|
+
private compressedSize: number = 0
|
|
59
|
+
private pendingCompressedChunk: Blob | null = null
|
|
60
|
+
private audioChunks: Float32Array[] = []
|
|
56
61
|
|
|
57
62
|
constructor({
|
|
58
63
|
audioContext,
|
|
@@ -121,6 +126,11 @@ export class WebRecorder {
|
|
|
121
126
|
if (recordingConfig.enableProcessing) {
|
|
122
127
|
this.initFeatureExtractorWorker()
|
|
123
128
|
}
|
|
129
|
+
|
|
130
|
+
// Initialize compressed recording if enabled
|
|
131
|
+
if (recordingConfig.compression?.enabled) {
|
|
132
|
+
this.initializeCompressedRecorder()
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
async init() {
|
|
@@ -155,18 +165,8 @@ export class WebRecorder {
|
|
|
155
165
|
return
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
//
|
|
159
|
-
this.
|
|
160
|
-
`Received audio blob from processor len:${pcmBufferFloat?.length}`,
|
|
161
|
-
event
|
|
162
|
-
)
|
|
163
|
-
// Concatenate the incoming Float32Array to the existing buffer
|
|
164
|
-
const newBuffer = new Float32Array(
|
|
165
|
-
this.audioBufferSize + pcmBufferFloat.length
|
|
166
|
-
)
|
|
167
|
-
newBuffer.set(this.audioBuffer, 0)
|
|
168
|
-
newBuffer.set(pcmBufferFloat, this.audioBufferSize)
|
|
169
|
-
this.audioBuffer = newBuffer
|
|
168
|
+
// Store chunks instead of concatenating
|
|
169
|
+
this.audioChunks.push(pcmBufferFloat)
|
|
170
170
|
this.audioBufferSize += pcmBufferFloat.length
|
|
171
171
|
|
|
172
172
|
const sampleRate =
|
|
@@ -197,9 +197,24 @@ export class WebRecorder {
|
|
|
197
197
|
// Track the number of packets
|
|
198
198
|
this.packetCount += 1
|
|
199
199
|
|
|
200
|
+
// Prepare compression data if available
|
|
201
|
+
let compressionData
|
|
202
|
+
if (this.pendingCompressedChunk) {
|
|
203
|
+
compressionData = {
|
|
204
|
+
data: this.pendingCompressedChunk,
|
|
205
|
+
size: this.pendingCompressedChunk.size,
|
|
206
|
+
totalSize: this.compressedSize,
|
|
207
|
+
mimeType: 'audio/webm',
|
|
208
|
+
format: 'opus',
|
|
209
|
+
bitrate: this.config.compression?.bitrate ?? 128000,
|
|
210
|
+
}
|
|
211
|
+
this.pendingCompressedChunk = null
|
|
212
|
+
}
|
|
213
|
+
|
|
200
214
|
this.emitAudioEventCallback({
|
|
201
|
-
data,
|
|
215
|
+
data: pcmBufferFloat,
|
|
202
216
|
position: this.position,
|
|
217
|
+
compression: compressionData,
|
|
203
218
|
})
|
|
204
219
|
this.position += duration // Update position
|
|
205
220
|
|
|
@@ -331,80 +346,175 @@ export class WebRecorder {
|
|
|
331
346
|
this.source.connect(this.audioWorkletNode)
|
|
332
347
|
this.audioWorkletNode.connect(this.audioContext.destination)
|
|
333
348
|
this.packetCount = 0
|
|
349
|
+
|
|
350
|
+
if (this.compressedMediaRecorder) {
|
|
351
|
+
this.compressedMediaRecorder.start(this.config.interval ?? 1000)
|
|
352
|
+
}
|
|
334
353
|
}
|
|
335
354
|
|
|
336
|
-
stop(): Promise<Float32Array> {
|
|
355
|
+
async stop(options?: WebRecordingOptions): Promise<{ pcmData: Float32Array; compressedBlob?: Blob }> {
|
|
337
356
|
return new Promise((resolve, reject) => {
|
|
338
|
-
|
|
339
|
-
if (this.audioWorkletNode) {
|
|
340
|
-
// this.source.disconnect(this.audioWorkletNode);
|
|
341
|
-
// this.audioWorkletNode.disconnect(this.audioContext.destination);
|
|
342
|
-
this.audioWorkletNode.port.postMessage({ command: 'stop' })
|
|
357
|
+
let isCleanupDone = false
|
|
343
358
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
359
|
+
const cleanup = () => {
|
|
360
|
+
if (isCleanupDone) return
|
|
361
|
+
isCleanupDone = true
|
|
362
|
+
|
|
363
|
+
if (this.audioContext) {
|
|
364
|
+
this.audioContext.close()
|
|
365
|
+
}
|
|
366
|
+
if (this.audioWorkletNode) {
|
|
367
|
+
this.audioWorkletNode.disconnect()
|
|
368
|
+
}
|
|
369
|
+
if (this.source) {
|
|
370
|
+
this.source.disconnect()
|
|
371
|
+
}
|
|
356
372
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
373
|
+
this.stopMediaStreamTracks()
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
this.logger?.debug(`Stopping WebRecorder ${this.audioChunks.length} chunks - this.compressedChunks.length=${this.compressedChunks.length}`)
|
|
377
|
+
|
|
378
|
+
// If skipFinalConsolidation is true and we have compressed data, return early
|
|
379
|
+
if (options?.skipFinalConsolidation && this.compressedMediaRecorder) {
|
|
380
|
+
this.compressedMediaRecorder.onstop = () => {
|
|
381
|
+
const compressedBlob = new Blob(this.compressedChunks, {
|
|
382
|
+
type: 'audio/webm;codecs=opus',
|
|
383
|
+
})
|
|
384
|
+
cleanup()
|
|
385
|
+
// Return the last chunk as pcmData to maintain interface compatibility
|
|
386
|
+
resolve({
|
|
387
|
+
pcmData: this.audioChunks[this.audioChunks.length - 1] || new Float32Array(),
|
|
388
|
+
compressedBlob,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
this.compressedMediaRecorder.stop()
|
|
392
|
+
return
|
|
393
|
+
}
|
|
362
394
|
|
|
363
|
-
|
|
364
|
-
|
|
395
|
+
// Handle compressed recording first
|
|
396
|
+
if (this.compressedMediaRecorder) {
|
|
397
|
+
this.compressedMediaRecorder.onstop = () => {
|
|
398
|
+
const compressedBlob = new Blob(this.compressedChunks, {
|
|
399
|
+
type: 'audio/webm;codecs=opus',
|
|
400
|
+
})
|
|
365
401
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
402
|
+
// Process chunks in batches
|
|
403
|
+
requestAnimationFrame(() => {
|
|
404
|
+
const combinedBuffer = new Float32Array(this.audioBufferSize)
|
|
405
|
+
let offset = 0
|
|
406
|
+
let chunkIndex = 0
|
|
407
|
+
const CHUNKS_PER_BATCH = 10 // Process 5 seconds worth of chunks at a time (10 * 500ms = 5s)
|
|
370
408
|
|
|
371
|
-
// Compute duration of the recorded data
|
|
372
|
-
const duration =
|
|
373
|
-
rawPCMDataFull.byteLength /
|
|
374
|
-
(this.audioContext.sampleRate *
|
|
375
|
-
(this.exportBitDepth /
|
|
376
|
-
this.numberOfChannels))
|
|
377
|
-
this.logger?.debug(
|
|
378
|
-
`Received recorded data -- Duration: ${duration} vs ${rawPCMDataFull.byteLength / this.audioContext.sampleRate} seconds`
|
|
379
|
-
)
|
|
380
409
|
this.logger?.debug(
|
|
381
|
-
`
|
|
410
|
+
`Processing ${this.audioChunks.length} chunks in batches of ${CHUNKS_PER_BATCH}`
|
|
382
411
|
)
|
|
383
412
|
|
|
384
|
-
|
|
385
|
-
|
|
413
|
+
const processNextBatch = () => {
|
|
414
|
+
const endIndex = Math.min(chunkIndex + CHUNKS_PER_BATCH, this.audioChunks.length)
|
|
415
|
+
|
|
416
|
+
// Process a batch of chunks
|
|
417
|
+
for (let i = chunkIndex; i < endIndex; i++) {
|
|
418
|
+
const chunk = this.audioChunks[i]
|
|
419
|
+
combinedBuffer.set(chunk, offset)
|
|
420
|
+
offset += chunk.length
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
chunkIndex = endIndex
|
|
424
|
+
|
|
425
|
+
if (chunkIndex < this.audioChunks.length) {
|
|
426
|
+
// Process next batch in next frame
|
|
427
|
+
requestAnimationFrame(processNextBatch)
|
|
428
|
+
} else {
|
|
429
|
+
// All chunks processed
|
|
430
|
+
this.audioBuffer = combinedBuffer
|
|
431
|
+
queueMicrotask(() => {
|
|
432
|
+
cleanup()
|
|
433
|
+
resolve({
|
|
434
|
+
pcmData: this.audioBuffer,
|
|
435
|
+
compressedBlob,
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
processNextBatch()
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
this.compressedMediaRecorder.stop()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Handle audio worklet data
|
|
448
|
+
if (this.audioWorkletNode) {
|
|
449
|
+
const onMessage = (event: AudioWorkletEvent) => {
|
|
450
|
+
if (event.data.command === 'recordedData') {
|
|
451
|
+
this.audioWorkletNode?.port.removeEventListener(
|
|
386
452
|
'message',
|
|
387
453
|
onMessage
|
|
388
454
|
)
|
|
389
455
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
456
|
+
queueMicrotask(async () => {
|
|
457
|
+
try {
|
|
458
|
+
const rawPCMDataFull =
|
|
459
|
+
event.data.recordedData?.slice(0)
|
|
460
|
+
if (!rawPCMDataFull) {
|
|
461
|
+
cleanup()
|
|
462
|
+
reject(
|
|
463
|
+
new Error(
|
|
464
|
+
'Failed to get recorded data'
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Convert PCM data if needed based on bit depth
|
|
471
|
+
if (this.exportBitDepth !== this.bitDepth) {
|
|
472
|
+
const convertedData =
|
|
473
|
+
await convertPCMToFloat32({
|
|
474
|
+
buffer: rawPCMDataFull.buffer,
|
|
475
|
+
bitDepth: this.exportBitDepth,
|
|
476
|
+
skipWavHeader: true,
|
|
477
|
+
logger: this.logger,
|
|
478
|
+
})
|
|
479
|
+
this.audioBuffer =
|
|
480
|
+
convertedData.pcmValues
|
|
481
|
+
} else {
|
|
482
|
+
this.audioBuffer = rawPCMDataFull
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!this.compressedMediaRecorder) {
|
|
486
|
+
cleanup()
|
|
487
|
+
resolve({ pcmData: this.audioBuffer })
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
cleanup()
|
|
491
|
+
reject(error)
|
|
492
|
+
}
|
|
394
493
|
})
|
|
395
|
-
|
|
396
|
-
resolve(convertedPCM.pcmValues) // Resolve the promise with the collected buffers
|
|
397
494
|
}
|
|
398
495
|
}
|
|
496
|
+
|
|
399
497
|
this.audioWorkletNode.port.addEventListener(
|
|
400
498
|
'message',
|
|
401
499
|
onMessage
|
|
402
500
|
)
|
|
403
|
-
|
|
501
|
+
this.audioWorkletNode.port.postMessage({ command: 'stop' })
|
|
404
502
|
|
|
405
|
-
|
|
406
|
-
|
|
503
|
+
// Safety timeout
|
|
504
|
+
setTimeout(() => {
|
|
505
|
+
this.audioWorkletNode?.port.removeEventListener(
|
|
506
|
+
'message',
|
|
507
|
+
onMessage
|
|
508
|
+
)
|
|
509
|
+
cleanup()
|
|
510
|
+
reject(new Error('Timeout while stopping recording'))
|
|
511
|
+
}, 5000)
|
|
512
|
+
} else if (!this.compressedMediaRecorder) {
|
|
513
|
+
cleanup()
|
|
514
|
+
resolve({ pcmData: this.audioBuffer })
|
|
515
|
+
}
|
|
407
516
|
} catch (error) {
|
|
517
|
+
cleanup()
|
|
408
518
|
reject(error)
|
|
409
519
|
}
|
|
410
520
|
})
|
|
@@ -414,6 +524,7 @@ export class WebRecorder {
|
|
|
414
524
|
this.source.disconnect(this.audioWorkletNode) // Disconnect the source from the AudioWorkletNode
|
|
415
525
|
this.audioWorkletNode.disconnect(this.audioContext.destination) // Disconnect the AudioWorkletNode from the destination
|
|
416
526
|
this.audioWorkletNode.port.postMessage({ command: 'pause' })
|
|
527
|
+
this.compressedMediaRecorder?.pause()
|
|
417
528
|
}
|
|
418
529
|
|
|
419
530
|
stopMediaStreamTracks() {
|
|
@@ -484,5 +595,40 @@ export class WebRecorder {
|
|
|
484
595
|
this.source.connect(this.audioWorkletNode)
|
|
485
596
|
this.audioWorkletNode.connect(this.audioContext.destination)
|
|
486
597
|
this.audioWorkletNode.port.postMessage({ command: 'resume' })
|
|
598
|
+
this.compressedMediaRecorder?.resume()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private initializeCompressedRecorder() {
|
|
602
|
+
try {
|
|
603
|
+
const mimeType = 'audio/webm;codecs=opus'
|
|
604
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
605
|
+
this.logger?.warn(
|
|
606
|
+
'Opus compression not supported in this browser'
|
|
607
|
+
)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.compressedMediaRecorder = new MediaRecorder(
|
|
612
|
+
this.source.mediaStream,
|
|
613
|
+
{
|
|
614
|
+
mimeType,
|
|
615
|
+
audioBitsPerSecond:
|
|
616
|
+
this.config.compression?.bitrate ?? 128000,
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
this.compressedMediaRecorder.ondataavailable = (event) => {
|
|
621
|
+
if (event.data.size > 0) {
|
|
622
|
+
this.compressedChunks.push(event.data)
|
|
623
|
+
this.compressedSize += event.data.size
|
|
624
|
+
this.pendingCompressedChunk = event.data
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.logger?.error(
|
|
629
|
+
'Failed to initialize compressed recorder:',
|
|
630
|
+
error
|
|
631
|
+
)
|
|
632
|
+
}
|
|
487
633
|
}
|
|
488
634
|
}
|
package/src/events.ts
CHANGED
|
@@ -7,6 +7,7 @@ import ExpoAudioStreamModule from './ExpoAudioStreamModule'
|
|
|
7
7
|
|
|
8
8
|
const emitter = new LegacyEventEmitter(ExpoAudioStreamModule)
|
|
9
9
|
|
|
10
|
+
// Internal event payload from native module
|
|
10
11
|
export interface AudioEventPayload {
|
|
11
12
|
encoded?: string
|
|
12
13
|
buffer?: Float32Array
|
|
@@ -17,6 +18,12 @@ export interface AudioEventPayload {
|
|
|
17
18
|
totalSize: number
|
|
18
19
|
mimeType: string
|
|
19
20
|
streamUuid: string
|
|
21
|
+
compression?: {
|
|
22
|
+
data?: string | Blob // Base64 (native) or Float32Array (web) encoded compressed data chunk
|
|
23
|
+
position: number
|
|
24
|
+
eventDataSize: number
|
|
25
|
+
totalSize: number
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export function addAudioEventListener(
|
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
AudioDataEvent,
|
|
8
8
|
AudioRecording,
|
|
9
9
|
AudioStreamStatus,
|
|
10
|
+
CompressionInfo,
|
|
10
11
|
ConsoleLike,
|
|
11
12
|
RecordingConfig,
|
|
12
13
|
StartRecordingResult,
|
|
14
|
+
WebRecordingOptions,
|
|
13
15
|
} from './ExpoAudioStream.types'
|
|
14
16
|
import ExpoAudioStreamModule from './ExpoAudioStreamModule'
|
|
15
17
|
import {
|
|
@@ -26,13 +28,14 @@ export interface UseAudioRecorderProps {
|
|
|
26
28
|
|
|
27
29
|
export interface UseAudioRecorderState {
|
|
28
30
|
startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>
|
|
29
|
-
stopRecording: () => Promise<AudioRecording>
|
|
31
|
+
stopRecording: (options?: WebRecordingOptions) => Promise<AudioRecording>
|
|
30
32
|
pauseRecording: () => Promise<void>
|
|
31
33
|
resumeRecording: () => Promise<void>
|
|
32
34
|
isRecording: boolean
|
|
33
35
|
isPaused: boolean
|
|
34
|
-
durationMs: number
|
|
35
|
-
size: number
|
|
36
|
+
durationMs: number
|
|
37
|
+
size: number
|
|
38
|
+
compression?: CompressionInfo
|
|
36
39
|
analysisData?: AudioAnalysis
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -41,6 +44,7 @@ interface RecorderReducerState {
|
|
|
41
44
|
isPaused: boolean
|
|
42
45
|
durationMs: number
|
|
43
46
|
size: number
|
|
47
|
+
compression?: CompressionInfo
|
|
44
48
|
analysisData?: AudioAnalysis
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -53,7 +57,14 @@ type RecorderAction =
|
|
|
53
57
|
isPaused: boolean
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
|
-
| {
|
|
60
|
+
| {
|
|
61
|
+
type: 'UPDATE_STATUS'
|
|
62
|
+
payload: {
|
|
63
|
+
durationMs: number
|
|
64
|
+
size: number
|
|
65
|
+
compression?: CompressionInfo
|
|
66
|
+
}
|
|
67
|
+
}
|
|
57
68
|
| { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
|
|
58
69
|
|
|
59
70
|
const defaultAnalysis: AudioAnalysis = {
|
|
@@ -84,7 +95,8 @@ function audioRecorderReducer(
|
|
|
84
95
|
isPaused: false,
|
|
85
96
|
durationMs: 0,
|
|
86
97
|
size: 0,
|
|
87
|
-
|
|
98
|
+
compression: undefined,
|
|
99
|
+
analysisData: defaultAnalysis,
|
|
88
100
|
}
|
|
89
101
|
case 'STOP':
|
|
90
102
|
return { ...state, isRecording: false, isPaused: false }
|
|
@@ -98,12 +110,22 @@ function audioRecorderReducer(
|
|
|
98
110
|
isPaused: action.payload.isPaused,
|
|
99
111
|
isRecording: action.payload.isRecording,
|
|
100
112
|
}
|
|
101
|
-
case 'UPDATE_STATUS':
|
|
102
|
-
|
|
113
|
+
case 'UPDATE_STATUS': {
|
|
114
|
+
const newState = {
|
|
103
115
|
...state,
|
|
104
116
|
durationMs: action.payload.durationMs,
|
|
105
117
|
size: action.payload.size,
|
|
118
|
+
compression: action.payload.compression
|
|
119
|
+
? {
|
|
120
|
+
size: action.payload.compression.size,
|
|
121
|
+
mimeType: action.payload.compression.mimeType,
|
|
122
|
+
bitrate: action.payload.compression.bitrate,
|
|
123
|
+
format: action.payload.compression.format,
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
106
126
|
}
|
|
127
|
+
return newState
|
|
128
|
+
}
|
|
107
129
|
case 'UPDATE_ANALYSIS':
|
|
108
130
|
return {
|
|
109
131
|
...state,
|
|
@@ -129,9 +151,12 @@ export function useAudioRecorder({
|
|
|
129
151
|
isPaused: false,
|
|
130
152
|
durationMs: 0,
|
|
131
153
|
size: 0,
|
|
154
|
+
compression: undefined,
|
|
132
155
|
analysisData: undefined,
|
|
133
156
|
})
|
|
134
157
|
|
|
158
|
+
const startResultRef = useRef<StartRecordingResult | null>(null)
|
|
159
|
+
|
|
135
160
|
const analysisListenerRef = useRef<EventSubscription | null>(null)
|
|
136
161
|
// analysisRef is the current analysis data (last 10 seconds by default)
|
|
137
162
|
const analysisRef = useRef<AudioAnalysis>({ ...defaultAnalysis })
|
|
@@ -261,6 +286,7 @@ export function useAudioRecorder({
|
|
|
261
286
|
encoded,
|
|
262
287
|
mimeType,
|
|
263
288
|
buffer,
|
|
289
|
+
compression,
|
|
264
290
|
} = eventData
|
|
265
291
|
logger?.debug(`[handleAudioEvent] Received audio event:`, {
|
|
266
292
|
fileUri,
|
|
@@ -271,6 +297,7 @@ export function useAudioRecorder({
|
|
|
271
297
|
lastEmittedSize,
|
|
272
298
|
streamUuid,
|
|
273
299
|
encodedLength: encoded?.length,
|
|
300
|
+
compression,
|
|
274
301
|
})
|
|
275
302
|
if (deltaSize === 0) {
|
|
276
303
|
// Ignore packet with no data
|
|
@@ -290,6 +317,21 @@ export function useAudioRecorder({
|
|
|
290
317
|
fileUri,
|
|
291
318
|
eventDataSize: deltaSize,
|
|
292
319
|
totalSize,
|
|
320
|
+
compression:
|
|
321
|
+
compression && startResultRef.current?.compression
|
|
322
|
+
? {
|
|
323
|
+
data: compression.data,
|
|
324
|
+
size: compression.totalSize,
|
|
325
|
+
mimeType:
|
|
326
|
+
startResultRef.current.compression
|
|
327
|
+
?.mimeType,
|
|
328
|
+
bitrate:
|
|
329
|
+
startResultRef.current.compression
|
|
330
|
+
?.bitrate,
|
|
331
|
+
format: startResultRef.current.compression
|
|
332
|
+
?.format,
|
|
333
|
+
}
|
|
334
|
+
: undefined,
|
|
293
335
|
})
|
|
294
336
|
} else if (buffer) {
|
|
295
337
|
// Coming from web
|
|
@@ -299,6 +341,21 @@ export function useAudioRecorder({
|
|
|
299
341
|
fileUri,
|
|
300
342
|
eventDataSize: deltaSize,
|
|
301
343
|
totalSize,
|
|
344
|
+
compression:
|
|
345
|
+
compression && startResultRef.current?.compression
|
|
346
|
+
? {
|
|
347
|
+
data: compression.data,
|
|
348
|
+
size: compression.totalSize,
|
|
349
|
+
mimeType:
|
|
350
|
+
startResultRef.current.compression
|
|
351
|
+
?.mimeType,
|
|
352
|
+
bitrate:
|
|
353
|
+
startResultRef.current.compression
|
|
354
|
+
?.bitrate,
|
|
355
|
+
format: startResultRef.current.compression
|
|
356
|
+
?.format,
|
|
357
|
+
}
|
|
358
|
+
: undefined,
|
|
302
359
|
}
|
|
303
360
|
onAudioStreamRef.current?.(webEvent)
|
|
304
361
|
logger?.debug(
|
|
@@ -317,7 +374,8 @@ export function useAudioRecorder({
|
|
|
317
374
|
try {
|
|
318
375
|
const status: AudioStreamStatus = ExpoAudioStream.status()
|
|
319
376
|
logger?.debug(
|
|
320
|
-
`Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}
|
|
377
|
+
`Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}`,
|
|
378
|
+
status.compression
|
|
321
379
|
)
|
|
322
380
|
|
|
323
381
|
// Check and update recording state
|
|
@@ -344,6 +402,7 @@ export function useAudioRecorder({
|
|
|
344
402
|
payload: {
|
|
345
403
|
durationMs: status.durationMs,
|
|
346
404
|
size: status.size,
|
|
405
|
+
compression: status.compression,
|
|
347
406
|
},
|
|
348
407
|
})
|
|
349
408
|
}
|
|
@@ -372,6 +431,8 @@ export function useAudioRecorder({
|
|
|
372
431
|
await ExpoAudioStream.startRecording(options)
|
|
373
432
|
dispatch({ type: 'START' })
|
|
374
433
|
|
|
434
|
+
startResultRef.current = startResult
|
|
435
|
+
|
|
375
436
|
if (enableProcessing) {
|
|
376
437
|
logger?.debug(`Enabling audio analysis listener`)
|
|
377
438
|
const listener = addAudioAnalysisListener(
|
|
@@ -466,6 +527,7 @@ export function useAudioRecorder({
|
|
|
466
527
|
isRecording: state.isRecording,
|
|
467
528
|
durationMs: state.durationMs,
|
|
468
529
|
size: state.size,
|
|
530
|
+
compression: state.compression,
|
|
469
531
|
analysisData: state.analysisData,
|
|
470
532
|
}
|
|
471
533
|
}
|