@siteed/expo-audio-stream 1.7.2 → 1.9.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +6 -1
  3. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +39 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +124 -12
  5. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +26 -2
  6. package/build/AudioRecorder.provider.d.ts.map +1 -1
  7. package/build/AudioRecorder.provider.js +1 -0
  8. package/build/AudioRecorder.provider.js.map +1 -1
  9. package/build/ExpoAudioStream.types.d.ts +22 -1
  10. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  11. package/build/ExpoAudioStream.types.js.map +1 -1
  12. package/build/ExpoAudioStream.web.d.ts +15 -2
  13. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  14. package/build/ExpoAudioStream.web.js +99 -40
  15. package/build/ExpoAudioStream.web.js.map +1 -1
  16. package/build/WebRecorder.web.d.ts +14 -3
  17. package/build/WebRecorder.web.d.ts.map +1 -1
  18. package/build/WebRecorder.web.js +188 -100
  19. package/build/WebRecorder.web.js.map +1 -1
  20. package/build/events.d.ts +6 -0
  21. package/build/events.d.ts.map +1 -1
  22. package/build/events.js.map +1 -1
  23. package/build/useAudioRecorder.d.ts +2 -1
  24. package/build/useAudioRecorder.d.ts.map +1 -1
  25. package/build/useAudioRecorder.js +46 -5
  26. package/build/useAudioRecorder.js.map +1 -1
  27. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  28. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  29. package/build/workers/inlineAudioWebWorker.web.js +65 -160
  30. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  31. package/ios/AudioStreamManager.swift +127 -8
  32. package/ios/AudioStreamManagerDelegate.swift +8 -2
  33. package/ios/ExpoAudioStreamModule.swift +61 -46
  34. package/ios/RecordingResult.swift +2 -0
  35. package/ios/RecordingSettings.swift +63 -3
  36. package/package.json +1 -1
  37. package/src/AudioRecorder.provider.tsx +1 -0
  38. package/src/ExpoAudioStream.types.ts +24 -1
  39. package/src/ExpoAudioStream.web.ts +111 -38
  40. package/src/WebRecorder.web.ts +238 -138
  41. package/src/events.ts +7 -0
  42. package/src/useAudioRecorder.tsx +68 -7
  43. package/src/workers/inlineAudioWebWorker.web.tsx +65 -160
@@ -35,6 +35,14 @@ const DEFAULT_ALGORITHM = 'rms'
35
35
 
36
36
  const TAG = 'WebRecorder'
37
37
 
38
+ const STOP_PERFORMANCE_MARKS = {
39
+ STOP_INITIATED: 'stopInitiated',
40
+ COMPRESSED_RECORDING_STOP: 'compressedRecordingStop',
41
+ AUDIO_WORKLET_STOP: 'audioWorkletStop',
42
+ CLEANUP: 'cleanup',
43
+ TOTAL_STOP_TIME: 'totalStopTime',
44
+ } as const
45
+
38
46
  export class WebRecorder {
39
47
  private audioContext: AudioContext
40
48
  private audioWorkletNode!: AudioWorkletNode
@@ -44,15 +52,18 @@ export class WebRecorder {
44
52
  private emitAudioEventCallback: EmitAudioEventFunction
45
53
  private emitAudioAnalysisCallback: EmitAudioAnalysisFunction
46
54
  private config: RecordingConfig
47
- private position: number // Track the cumulative position
55
+ private position: number = 0
48
56
  private numberOfChannels: number // Number of audio channels
49
57
  private bitDepth: number // Bit depth of the audio
50
58
  private exportBitDepth: number // Bit depth of the audio
51
- private audioBuffer: Float32Array // Single buffer to store the audio data
52
- private audioBufferSize: number // Keep track of the buffer size
53
59
  private audioAnalysisData: AudioAnalysis // Keep updating the full audio analysis data with latest events
54
60
  private packetCount: number = 0
55
61
  private logger?: ConsoleLike
62
+ private compressedMediaRecorder: MediaRecorder | null = null
63
+ private compressedChunks: Blob[] = []
64
+ private compressedSize: number = 0
65
+ private pendingCompressedChunk: Blob | null = null
66
+ private readonly wavMimeType = 'audio/wav'
56
67
 
57
68
  constructor({
58
69
  audioContext,
@@ -77,7 +88,6 @@ export class WebRecorder {
77
88
  this.emitAudioEventCallback = emitAudioEventCallback
78
89
  this.emitAudioAnalysisCallback = emitAudioAnalysisCallback
79
90
  this.config = recordingConfig
80
- this.position = 0
81
91
  this.logger = logger
82
92
 
83
93
  const audioContextFormat = this.checkAudioContextFormat({
@@ -100,10 +110,6 @@ export class WebRecorder {
100
110
  audioContextFormat.bitDepth ||
101
111
  DEFAULT_WEB_BITDEPTH
102
112
 
103
- // Initialize the audio buffer separately
104
- this.audioBuffer = new Float32Array(0)
105
- this.audioBufferSize = 0
106
-
107
113
  this.audioAnalysisData = {
108
114
  amplitudeRange: { min: 0, max: 0 },
109
115
  dataPoints: [],
@@ -121,6 +127,11 @@ export class WebRecorder {
121
127
  if (recordingConfig.enableProcessing) {
122
128
  this.initFeatureExtractorWorker()
123
129
  }
130
+
131
+ // Initialize compressed recording if enabled
132
+ if (recordingConfig.compression?.enabled) {
133
+ this.initializeCompressedRecorder()
134
+ }
124
135
  }
125
136
 
126
137
  async init() {
@@ -145,80 +156,69 @@ export class WebRecorder {
145
156
  event: AudioWorkletEvent
146
157
  ) => {
147
158
  const command = event.data.command
148
- if (command !== 'newData') {
149
- return
150
- }
151
- const pcmBufferFloat = event.data.recordedData
159
+ if (command !== 'newData') return
152
160
 
161
+ const pcmBufferFloat = event.data.recordedData
153
162
  if (!pcmBufferFloat) {
154
163
  this.logger?.warn('Received empty audio buffer', event)
155
164
  return
156
165
  }
157
166
 
158
- // Handle the audio blob (e.g., send it to the server or process it further)
159
- this.logger?.debug(
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
170
- this.audioBufferSize += pcmBufferFloat.length
171
-
167
+ // Process data in smaller chunks and emit immediately
168
+ const chunkSize = this.audioContext.sampleRate * 2 // Reduce to 2 seconds chunks
172
169
  const sampleRate =
173
170
  event.data.sampleRate ?? this.audioContext.sampleRate
174
- const duration = pcmBufferFloat.length / sampleRate // Calculate duration of the current buffer
175
-
176
- let data: Float32Array
177
- if (this.packetCount === 0) {
178
- // Initialize WAV header
179
- const wavHeaderBuffer = writeWavHeader({
180
- sampleRate: this.audioContext.sampleRate,
181
- numChannels: this.numberOfChannels,
182
- bitDepth: this.exportBitDepth,
183
- })
171
+ const duration = pcmBufferFloat.length / sampleRate
172
+
173
+ // Emit chunks without storing them
174
+ for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
175
+ const chunk = pcmBufferFloat.slice(i, i + chunkSize)
176
+ const chunkPosition = this.position + i / sampleRate
177
+
178
+ // Process features if enabled
179
+ if (
180
+ this.config.enableProcessing &&
181
+ this.featureExtractorWorker
182
+ ) {
183
+ this.featureExtractorWorker.postMessage(
184
+ {
185
+ command: 'process',
186
+ channelData: chunk,
187
+ sampleRate,
188
+ pointsPerSecond:
189
+ this.config.pointsPerSecond ||
190
+ DEFAULT_WEB_POINTS_PER_SECOND,
191
+ algorithm: this.config.algorithm || 'rms',
192
+ bitDepth: this.bitDepth,
193
+ fullAudioDurationMs: this.position * 1000,
194
+ numberOfChannels: this.numberOfChannels,
195
+ features: this.config.features,
196
+ },
197
+ []
198
+ )
199
+ }
184
200
 
185
- // For the first packet, combine WAV header with audio data
186
- const headerFloatArray = new Float32Array(wavHeaderBuffer)
187
- data = new Float32Array(
188
- headerFloatArray.length + this.audioBuffer.length
189
- )
190
- data.set(headerFloatArray, 0)
191
- data.set(this.audioBuffer, headerFloatArray.length)
192
- } else {
193
- // For subsequent packets, just send the new audio data
194
- data = pcmBufferFloat
201
+ // Emit chunk immediately
202
+ this.emitAudioEventCallback({
203
+ data: chunk,
204
+ position: chunkPosition,
205
+ compression: this.pendingCompressedChunk
206
+ ? {
207
+ data: this.pendingCompressedChunk,
208
+ size: this.pendingCompressedChunk.size,
209
+ totalSize: this.compressedSize,
210
+ mimeType: 'audio/webm',
211
+ format: 'opus',
212
+ bitrate:
213
+ this.config.compression?.bitrate ??
214
+ 128000,
215
+ }
216
+ : undefined,
217
+ })
195
218
  }
196
219
 
197
- // Track the number of packets
198
- this.packetCount += 1
199
-
200
- this.emitAudioEventCallback({
201
- data,
202
- position: this.position,
203
- })
204
- this.position += duration // Update position
205
-
206
- this.featureExtractorWorker?.postMessage(
207
- {
208
- command: 'process',
209
- channelData: pcmBufferFloat,
210
- sampleRate,
211
- pointsPerSecond:
212
- this.config.pointsPerSecond ||
213
- DEFAULT_WEB_POINTS_PER_SECOND,
214
- algorithm: this.config.algorithm || 'rms',
215
- bitDepth: this.bitDepth,
216
- fullAudioDurationMs: this.position * 1000,
217
- numberOfChannels: this.numberOfChannels,
218
- features: this.config.features,
219
- },
220
- []
221
- )
220
+ this.position += duration
221
+ this.pendingCompressedChunk = null
222
222
  }
223
223
 
224
224
  this.logger?.debug(
@@ -227,7 +227,7 @@ export class WebRecorder {
227
227
  )
228
228
  this.audioWorkletNode.port.postMessage({
229
229
  command: 'init',
230
- recordSampleRate: this.audioContext.sampleRate, // Pass the original sample rate
230
+ recordSampleRate: this.audioContext.sampleRate,
231
231
  exportSampleRate:
232
232
  this.config.sampleRate ?? this.audioContext.sampleRate,
233
233
  bitDepth: this.bitDepth,
@@ -331,82 +331,146 @@ export class WebRecorder {
331
331
  this.source.connect(this.audioWorkletNode)
332
332
  this.audioWorkletNode.connect(this.audioContext.destination)
333
333
  this.packetCount = 0
334
+
335
+ if (this.compressedMediaRecorder) {
336
+ this.compressedMediaRecorder.start(this.config.interval ?? 1000)
337
+ }
334
338
  }
335
339
 
336
- stop(): Promise<Float32Array> {
337
- return new Promise((resolve, reject) => {
338
- try {
339
- if (this.audioWorkletNode) {
340
- // this.source.disconnect(this.audioWorkletNode);
341
- // this.audioWorkletNode.disconnect(this.audioContext.destination);
342
- this.audioWorkletNode.port.postMessage({ command: 'stop' })
343
-
344
- // Set a timeout to reject the promise if no message is received within 5 seconds
345
- const timeout = setTimeout(() => {
346
- this.audioWorkletNode.port.removeEventListener(
347
- 'message',
348
- onMessage
349
- )
350
- reject(
351
- new Error(
352
- "Timeout error, audioWorkletNode didn't complete."
353
- )
354
- )
355
- }, 5000)
356
-
357
- // Listen for the recordedData message to confirm stopping
358
- const onMessage = async (event: AudioWorkletEvent) => {
359
- const command = event.data.command
360
- if (command === 'recordedData') {
361
- clearTimeout(timeout) // Clear the timeout
362
-
363
- const rawPCMDataFull =
364
- event.data.recordedData?.slice(0)
365
-
366
- if (!rawPCMDataFull) {
367
- reject(new Error('Failed to get recorded data'))
368
- return
369
- }
370
-
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
- this.logger?.debug(
381
- `recordedData.length=${rawPCMDataFull.byteLength} vs transmittedData.length=${this.audioBufferSize}`
382
- )
340
+ async stop(): Promise<{ pcmData: Float32Array; compressedBlob?: Blob }> {
341
+ try {
342
+ if (this.compressedMediaRecorder) {
343
+ this.compressedMediaRecorder.stop()
344
+ return {
345
+ pcmData: new Float32Array(), // Return empty array since we're streaming
346
+ compressedBlob: new Blob(this.compressedChunks, {
347
+ type: 'audio/webm;codecs=opus',
348
+ }),
349
+ }
350
+ }
351
+ return { pcmData: new Float32Array() }
352
+ } finally {
353
+ this.cleanup()
354
+ }
355
+ }
383
356
 
384
- // Remove the event listener after receiving the final data
385
- this.audioWorkletNode.port.removeEventListener(
386
- 'message',
387
- onMessage
388
- )
357
+ private cleanup() {
358
+ if (this.audioContext) {
359
+ this.audioContext.close()
360
+ }
361
+ if (this.audioWorkletNode) {
362
+ this.audioWorkletNode.disconnect()
363
+ }
364
+ if (this.source) {
365
+ this.source.disconnect()
366
+ }
367
+ this.stopMediaStreamTracks()
368
+ }
389
369
 
390
- const convertedPCM = await convertPCMToFloat32({
391
- buffer: rawPCMDataFull.buffer,
392
- skipWavHeader: true,
393
- bitDepth: this.exportBitDepth,
394
- })
370
+ // Helper method to process recording stop
371
+ private async processRecordingStop(): Promise<{
372
+ pcmData: Float32Array
373
+ compressedBlob?: Blob
374
+ }> {
375
+ const processStartTime = performance.now()
376
+ this.logger?.debug('[Performance] Starting recording stop process')
377
+
378
+ const [compressedData, workletData] = await Promise.all([
379
+ this.stopCompressedRecording(),
380
+ this.stopAudioWorklet(),
381
+ ])
382
+
383
+ this.logger?.debug(
384
+ `[Performance] Recording stop process completed in ${performance.now() - processStartTime}ms`
385
+ )
386
+ return {
387
+ pcmData:
388
+ workletData ??
389
+ new Float32Array(this.audioAnalysisData.dataPoints.length),
390
+ compressedBlob: compressedData,
391
+ }
392
+ }
395
393
 
396
- resolve(convertedPCM.pcmValues) // Resolve the promise with the collected buffers
397
- }
398
- }
399
- this.audioWorkletNode.port.addEventListener(
394
+ // Helper method to stop compressed recording
395
+ private stopCompressedRecording(): Promise<Blob | undefined> {
396
+ const startTime = performance.now()
397
+ this.logger?.debug(
398
+ `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Starting compressed recording stop`
399
+ )
400
+
401
+ if (!this.compressedMediaRecorder) {
402
+ this.logger?.debug('[Performance] No compressed recorder to stop')
403
+ return Promise.resolve(undefined)
404
+ }
405
+
406
+ return new Promise((resolve) => {
407
+ this.compressedMediaRecorder!.onstop = () => {
408
+ const blob = new Blob(this.compressedChunks, {
409
+ type: 'audio/webm;codecs=opus',
410
+ })
411
+ this.logger?.debug(
412
+ `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Compressed recording stopped in ${performance.now() - startTime}ms, size: ${blob.size}`
413
+ )
414
+ resolve(blob)
415
+ }
416
+ this.compressedMediaRecorder!.stop()
417
+ })
418
+ }
419
+
420
+ // Helper method to stop audio worklet
421
+ private stopAudioWorklet(): Promise<Float32Array | undefined> {
422
+ const startTime = performance.now()
423
+ this.logger?.debug(
424
+ `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Starting audio worklet stop`
425
+ )
426
+
427
+ if (!this.audioWorkletNode) {
428
+ this.logger?.debug('[Performance] No audio worklet to stop')
429
+ return Promise.resolve(undefined)
430
+ }
431
+
432
+ return new Promise((resolve) => {
433
+ const onMessage = (event: AudioWorkletEvent) => {
434
+ if (event.data.command === 'recordedData') {
435
+ this.audioWorkletNode?.port.removeEventListener(
400
436
  'message',
401
437
  onMessage
402
438
  )
403
- }
439
+ const rawPCMDataFull = event.data.recordedData?.slice(0)
440
+
441
+ if (!rawPCMDataFull) {
442
+ this.logger?.debug('[Performance] No PCM data received')
443
+ resolve(undefined)
444
+ return
445
+ }
404
446
 
405
- // Stop all media stream tracks to stop the browser recording
406
- this.stopMediaStreamTracks()
407
- } catch (error) {
408
- reject(error)
447
+ if (this.exportBitDepth !== this.bitDepth) {
448
+ const conversionStart = performance.now()
449
+ convertPCMToFloat32({
450
+ buffer: rawPCMDataFull.buffer,
451
+ bitDepth: this.exportBitDepth,
452
+ skipWavHeader: true,
453
+ logger: this.logger,
454
+ }).then(({ pcmValues }) => {
455
+ this.logger?.debug(
456
+ `[Performance] PCM conversion completed in ${performance.now() - conversionStart}ms`
457
+ )
458
+ this.logger?.debug(
459
+ `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
460
+ )
461
+ resolve(pcmValues)
462
+ })
463
+ } else {
464
+ this.logger?.debug(
465
+ `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
466
+ )
467
+ resolve(rawPCMDataFull)
468
+ }
469
+ }
409
470
  }
471
+
472
+ this.audioWorkletNode.port.addEventListener('message', onMessage)
473
+ this.audioWorkletNode.port.postMessage({ command: 'stop' })
410
474
  })
411
475
  }
412
476
 
@@ -414,6 +478,7 @@ export class WebRecorder {
414
478
  this.source.disconnect(this.audioWorkletNode) // Disconnect the source from the AudioWorkletNode
415
479
  this.audioWorkletNode.disconnect(this.audioContext.destination) // Disconnect the AudioWorkletNode from the destination
416
480
  this.audioWorkletNode.port.postMessage({ command: 'pause' })
481
+ this.compressedMediaRecorder?.pause()
417
482
  }
418
483
 
419
484
  stopMediaStreamTracks() {
@@ -484,5 +549,40 @@ export class WebRecorder {
484
549
  this.source.connect(this.audioWorkletNode)
485
550
  this.audioWorkletNode.connect(this.audioContext.destination)
486
551
  this.audioWorkletNode.port.postMessage({ command: 'resume' })
552
+ this.compressedMediaRecorder?.resume()
553
+ }
554
+
555
+ private initializeCompressedRecorder() {
556
+ try {
557
+ const mimeType = 'audio/webm;codecs=opus'
558
+ if (!MediaRecorder.isTypeSupported(mimeType)) {
559
+ this.logger?.warn(
560
+ 'Opus compression not supported in this browser'
561
+ )
562
+ return
563
+ }
564
+
565
+ this.compressedMediaRecorder = new MediaRecorder(
566
+ this.source.mediaStream,
567
+ {
568
+ mimeType,
569
+ audioBitsPerSecond:
570
+ this.config.compression?.bitrate ?? 128000,
571
+ }
572
+ )
573
+
574
+ this.compressedMediaRecorder.ondataavailable = (event) => {
575
+ if (event.data.size > 0) {
576
+ this.compressedChunks.push(event.data)
577
+ this.compressedSize += event.data.size
578
+ this.pendingCompressedChunk = event.data
579
+ }
580
+ }
581
+ } catch (error) {
582
+ this.logger?.error(
583
+ 'Failed to initialize compressed recorder:',
584
+ error
585
+ )
586
+ }
487
587
  }
488
588
  }
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(
@@ -7,6 +7,7 @@ import {
7
7
  AudioDataEvent,
8
8
  AudioRecording,
9
9
  AudioStreamStatus,
10
+ CompressionInfo,
10
11
  ConsoleLike,
11
12
  RecordingConfig,
12
13
  StartRecordingResult,
@@ -31,8 +32,9 @@ export interface UseAudioRecorderState {
31
32
  resumeRecording: () => Promise<void>
32
33
  isRecording: boolean
33
34
  isPaused: boolean
34
- durationMs: number // Duration of the recording
35
- size: number // Size in bytes of the recorded audio
35
+ durationMs: number
36
+ size: number
37
+ compression?: CompressionInfo
36
38
  analysisData?: AudioAnalysis
37
39
  }
38
40
 
@@ -41,6 +43,7 @@ interface RecorderReducerState {
41
43
  isPaused: boolean
42
44
  durationMs: number
43
45
  size: number
46
+ compression?: CompressionInfo
44
47
  analysisData?: AudioAnalysis
45
48
  }
46
49
 
@@ -53,7 +56,14 @@ type RecorderAction =
53
56
  isPaused: boolean
54
57
  }
55
58
  }
56
- | { type: 'UPDATE_STATUS'; payload: { durationMs: number; size: number } }
59
+ | {
60
+ type: 'UPDATE_STATUS'
61
+ payload: {
62
+ durationMs: number
63
+ size: number
64
+ compression?: CompressionInfo
65
+ }
66
+ }
57
67
  | { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
58
68
 
59
69
  const defaultAnalysis: AudioAnalysis = {
@@ -84,7 +94,8 @@ function audioRecorderReducer(
84
94
  isPaused: false,
85
95
  durationMs: 0,
86
96
  size: 0,
87
- analysisData: defaultAnalysis, // Reset analysis data
97
+ compression: undefined,
98
+ analysisData: defaultAnalysis,
88
99
  }
89
100
  case 'STOP':
90
101
  return { ...state, isRecording: false, isPaused: false }
@@ -98,12 +109,22 @@ function audioRecorderReducer(
98
109
  isPaused: action.payload.isPaused,
99
110
  isRecording: action.payload.isRecording,
100
111
  }
101
- case 'UPDATE_STATUS':
102
- return {
112
+ case 'UPDATE_STATUS': {
113
+ const newState = {
103
114
  ...state,
104
115
  durationMs: action.payload.durationMs,
105
116
  size: action.payload.size,
117
+ compression: action.payload.compression
118
+ ? {
119
+ size: action.payload.compression.size,
120
+ mimeType: action.payload.compression.mimeType,
121
+ bitrate: action.payload.compression.bitrate,
122
+ format: action.payload.compression.format,
123
+ }
124
+ : undefined,
106
125
  }
126
+ return newState
127
+ }
107
128
  case 'UPDATE_ANALYSIS':
108
129
  return {
109
130
  ...state,
@@ -129,9 +150,12 @@ export function useAudioRecorder({
129
150
  isPaused: false,
130
151
  durationMs: 0,
131
152
  size: 0,
153
+ compression: undefined,
132
154
  analysisData: undefined,
133
155
  })
134
156
 
157
+ const startResultRef = useRef<StartRecordingResult | null>(null)
158
+
135
159
  const analysisListenerRef = useRef<EventSubscription | null>(null)
136
160
  // analysisRef is the current analysis data (last 10 seconds by default)
137
161
  const analysisRef = useRef<AudioAnalysis>({ ...defaultAnalysis })
@@ -261,6 +285,7 @@ export function useAudioRecorder({
261
285
  encoded,
262
286
  mimeType,
263
287
  buffer,
288
+ compression,
264
289
  } = eventData
265
290
  logger?.debug(`[handleAudioEvent] Received audio event:`, {
266
291
  fileUri,
@@ -271,6 +296,7 @@ export function useAudioRecorder({
271
296
  lastEmittedSize,
272
297
  streamUuid,
273
298
  encodedLength: encoded?.length,
299
+ compression,
274
300
  })
275
301
  if (deltaSize === 0) {
276
302
  // Ignore packet with no data
@@ -290,6 +316,21 @@ export function useAudioRecorder({
290
316
  fileUri,
291
317
  eventDataSize: deltaSize,
292
318
  totalSize,
319
+ compression:
320
+ compression && startResultRef.current?.compression
321
+ ? {
322
+ data: compression.data,
323
+ size: compression.totalSize,
324
+ mimeType:
325
+ startResultRef.current.compression
326
+ ?.mimeType,
327
+ bitrate:
328
+ startResultRef.current.compression
329
+ ?.bitrate,
330
+ format: startResultRef.current.compression
331
+ ?.format,
332
+ }
333
+ : undefined,
293
334
  })
294
335
  } else if (buffer) {
295
336
  // Coming from web
@@ -299,6 +340,21 @@ export function useAudioRecorder({
299
340
  fileUri,
300
341
  eventDataSize: deltaSize,
301
342
  totalSize,
343
+ compression:
344
+ compression && startResultRef.current?.compression
345
+ ? {
346
+ data: compression.data,
347
+ size: compression.totalSize,
348
+ mimeType:
349
+ startResultRef.current.compression
350
+ ?.mimeType,
351
+ bitrate:
352
+ startResultRef.current.compression
353
+ ?.bitrate,
354
+ format: startResultRef.current.compression
355
+ ?.format,
356
+ }
357
+ : undefined,
302
358
  }
303
359
  onAudioStreamRef.current?.(webEvent)
304
360
  logger?.debug(
@@ -317,7 +373,8 @@ export function useAudioRecorder({
317
373
  try {
318
374
  const status: AudioStreamStatus = ExpoAudioStream.status()
319
375
  logger?.debug(
320
- `Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}`
376
+ `Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}`,
377
+ status.compression
321
378
  )
322
379
 
323
380
  // Check and update recording state
@@ -344,6 +401,7 @@ export function useAudioRecorder({
344
401
  payload: {
345
402
  durationMs: status.durationMs,
346
403
  size: status.size,
404
+ compression: status.compression,
347
405
  },
348
406
  })
349
407
  }
@@ -372,6 +430,8 @@ export function useAudioRecorder({
372
430
  await ExpoAudioStream.startRecording(options)
373
431
  dispatch({ type: 'START' })
374
432
 
433
+ startResultRef.current = startResult
434
+
375
435
  if (enableProcessing) {
376
436
  logger?.debug(`Enabling audio analysis listener`)
377
437
  const listener = addAudioAnalysisListener(
@@ -466,6 +526,7 @@ export function useAudioRecorder({
466
526
  isRecording: state.isRecording,
467
527
  durationMs: state.durationMs,
468
528
  size: state.size,
529
+ compression: state.compression,
469
530
  analysisData: state.analysisData,
470
531
  }
471
532
  }