@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
@@ -13,11 +13,18 @@ import {
13
13
  import { WebRecorder } from './WebRecorder.web'
14
14
  import { AudioEventPayload } from './events'
15
15
  import { encodingToBitDepth } from './utils/encodingToBitDepth'
16
- import { writeWavHeader } from './utils/writeWavHeader'
17
16
 
18
17
  export interface EmitAudioEventProps {
19
18
  data: Float32Array
20
19
  position: number
20
+ compression?: {
21
+ data: Blob
22
+ size: number
23
+ totalSize: number
24
+ mimeType: string
25
+ format: string
26
+ bitrate: number
27
+ }
21
28
  }
22
29
  export type EmitAudioEventFunction = (_: EmitAudioEventProps) => void
23
30
  export type EmitAudioAnalysisFunction = (_: AudioAnalysis) => void
@@ -26,6 +33,7 @@ export interface ExpoAudioStreamWebProps {
26
33
  logger?: ConsoleLike
27
34
  audioWorkletUrl: string
28
35
  featuresExtratorUrl: string
36
+ maxBufferSize?: number // Maximum number of chunks to keep in memory
29
37
  }
30
38
 
31
39
  export class ExpoAudioStreamWeb extends LegacyEventEmitter {
@@ -40,6 +48,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
40
48
  currentInterval: number
41
49
  lastEmittedSize: number
42
50
  lastEmittedTime: number
51
+ lastEmittedCompressionSize: number
43
52
  streamUuid: string | null
44
53
  extension: 'webm' | 'wav' = 'wav' // Default extension is 'webm'
45
54
  recordingConfig?: RecordingConfig
@@ -47,11 +56,15 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
47
56
  audioWorkletUrl: string
48
57
  featuresExtratorUrl: string
49
58
  logger?: ConsoleLike
59
+ latestPosition: number = 0
60
+ totalCompressedSize: number = 0
61
+ private readonly maxBufferSize: number
50
62
 
51
63
  constructor({
52
64
  audioWorkletUrl,
53
65
  featuresExtratorUrl,
54
66
  logger,
67
+ maxBufferSize = 100, // Default to storing last 100 chunks (1 chunk = 0.5 seconds)
55
68
  }: ExpoAudioStreamWebProps) {
56
69
  const mockNativeModule = {
57
70
  addListener: () => {
@@ -76,9 +89,12 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
76
89
  this.currentInterval = 1000 // Default interval in ms
77
90
  this.lastEmittedSize = 0
78
91
  this.lastEmittedTime = 0
92
+ this.latestPosition = 0
93
+ this.lastEmittedCompressionSize = 0
79
94
  this.streamUuid = null // Initialize UUID on first recording start
80
95
  this.audioWorkletUrl = audioWorkletUrl
81
96
  this.featuresExtratorUrl = featuresExtratorUrl
97
+ this.maxBufferSize = maxBufferSize
82
98
  }
83
99
 
84
100
  // Utility to handle user media stream
@@ -86,7 +102,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
86
102
  try {
87
103
  return await navigator.mediaDevices.getUserMedia({ audio: true })
88
104
  } catch (error) {
89
- console.error('Failed to get media stream:', error)
105
+ this.logger?.error('Failed to get media stream:', error)
90
106
  throw error
91
107
  }
92
108
  }
@@ -110,6 +126,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
110
126
  const source = audioContext.createMediaStreamSource(stream)
111
127
 
112
128
  this.customRecorder = new WebRecorder({
129
+ logger: this.logger,
113
130
  audioContext,
114
131
  source,
115
132
  recordingConfig,
@@ -117,12 +134,18 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
117
134
  emitAudioEventCallback: ({
118
135
  data,
119
136
  position,
137
+ compression,
120
138
  }: EmitAudioEventProps) => {
139
+ // Keep only the latest chunks based on maxBufferSize
121
140
  this.audioChunks.push(new Float32Array(data))
141
+ if (this.audioChunks.length > this.maxBufferSize) {
142
+ this.audioChunks.shift() // Remove oldest chunk
143
+ }
122
144
  this.currentSize += data.byteLength
123
- this.emitAudioEvent({ data, position })
145
+ this.emitAudioEvent({ data, position, compression })
124
146
  this.lastEmittedTime = Date.now()
125
147
  this.lastEmittedSize = this.currentSize
148
+ this.lastEmittedCompressionSize = compression?.size ?? 0
126
149
  },
127
150
  emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysis) => {
128
151
  this.logger?.log(`Emitted AudioAnalysis:`, audioAnalysisData)
@@ -146,6 +169,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
146
169
  this.isPaused = false
147
170
  this.lastEmittedSize = 0
148
171
  this.lastEmittedTime = 0
172
+ this.lastEmittedCompressionSize = 0
149
173
  this.streamUuid = Date.now().toString()
150
174
  const fileUri = `${this.streamUuid}.${this.extension}`
151
175
  const streamConfig: StartRecordingResult = {
@@ -154,21 +178,46 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
154
178
  bitDepth: this.bitDepth,
155
179
  channels: recordingConfig.channels ?? 1,
156
180
  sampleRate: recordingConfig.sampleRate ?? 44100,
181
+ compression: recordingConfig.compression
182
+ ? {
183
+ ...recordingConfig.compression,
184
+ bitrate: recordingConfig.compression?.bitrate ?? 128000,
185
+ size: 0,
186
+ mimeType: 'audio/webm',
187
+ format: recordingConfig.compression?.format ?? 'opus',
188
+ compressedFileUri: '',
189
+ }
190
+ : undefined,
157
191
  }
158
192
  return streamConfig
159
193
  }
160
194
 
161
- emitAudioEvent({ data, position }: EmitAudioEventProps) {
195
+ emitAudioEvent({ data, position, compression }: EmitAudioEventProps) {
162
196
  const fileUri = `${this.streamUuid}.${this.extension}`
197
+ if (compression?.size) {
198
+ this.lastEmittedCompressionSize = compression.size
199
+ this.totalCompressedSize = compression.totalSize
200
+ }
201
+ this.latestPosition = position
202
+ this.currentDurationMs = position * 1000 // Convert position (in seconds) to ms
203
+
163
204
  const audioEventPayload: AudioEventPayload = {
164
205
  fileUri,
165
206
  mimeType: `audio/${this.extension}`,
166
- lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
207
+ lastEmittedSize: this.lastEmittedSize,
167
208
  deltaSize: data.byteLength,
168
209
  position,
169
210
  totalSize: this.currentSize,
170
211
  buffer: data,
171
- streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification
212
+ streamUuid: this.streamUuid ?? '',
213
+ compression: compression
214
+ ? {
215
+ data: compression?.data,
216
+ totalSize: this.totalCompressedSize,
217
+ eventDataSize: compression?.size ?? 0,
218
+ position,
219
+ }
220
+ : undefined,
172
221
  }
173
222
 
174
223
  this.emit('AudioData', audioEventPayload)
@@ -180,43 +229,58 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
180
229
  throw new Error('Recorder is not initialized')
181
230
  }
182
231
 
183
- const fullPcmBufferArray = await this.customRecorder.stop()
232
+ this.logger?.debug('[Stop] Starting stop process')
233
+ const startTime = performance.now()
184
234
 
185
- this.logger?.debug(`Stopped recording`, fullPcmBufferArray)
186
- this.isRecording = false
187
- this.isPaused = false
188
- this.currentDurationMs = Date.now() - this.recordingStartTime
235
+ try {
236
+ this.logger?.debug('[Stop] Stopping recorder')
237
+ const { compressedBlob } = await this.customRecorder.stop()
189
238
 
190
- // Create WAV header and combine with PCM data in one step
191
- const wavBuffer = writeWavHeader({
192
- buffer: fullPcmBufferArray.buffer,
193
- sampleRate: this.recordingConfig?.sampleRate ?? 44100,
194
- numChannels: this.recordingConfig?.channels ?? 1,
195
- bitDepth: this.bitDepth,
196
- })
239
+ this.isRecording = false
240
+ this.isPaused = false
241
+ this.currentDurationMs = Date.now() - this.recordingStartTime
197
242
 
198
- // Create a cloneable copy
199
- const cloneableBuffer = wavBuffer.slice(0)
243
+ let compression: AudioRecording['compression']
244
+ let fileUri = `${this.streamUuid}.${this.extension}`
245
+ let mimeType = `audio/${this.extension}`
200
246
 
201
- // Create blob with complete WAV data
202
- const blob = new Blob([cloneableBuffer], {
203
- type: `audio/${this.extension}`,
204
- })
205
- const fileUri = URL.createObjectURL(blob)
247
+ if (compressedBlob && this.recordingConfig?.compression?.enabled) {
248
+ const compressedUri = URL.createObjectURL(compressedBlob)
249
+ compression = {
250
+ compressedFileUri: compressedUri,
251
+ size: compressedBlob.size,
252
+ mimeType: 'audio/webm',
253
+ format: 'opus',
254
+ bitrate: this.recordingConfig.compression.bitrate ?? 128000,
255
+ }
256
+ // Use compressed values when compression is enabled
257
+ fileUri = compressedUri
258
+ mimeType = 'audio/webm'
259
+ }
206
260
 
207
- const result: AudioRecording = {
208
- fileUri,
209
- filename: `${this.streamUuid}.${this.extension}`,
210
- wavPCMData: new Float32Array(cloneableBuffer),
211
- bitDepth: this.bitDepth,
212
- channels: this.recordingConfig?.channels ?? 1,
213
- sampleRate: this.recordingConfig?.sampleRate ?? 44100,
214
- durationMs: this.currentDurationMs,
215
- size: this.currentSize,
216
- mimeType: `audio/${this.extension}`,
217
- }
261
+ this.logger?.debug(
262
+ `[Stop] Completed stop process in ${performance.now() - startTime}ms`,
263
+ {
264
+ durationMs: this.currentDurationMs,
265
+ compressedSize: compression?.size,
266
+ }
267
+ )
218
268
 
219
- return result
269
+ return {
270
+ fileUri,
271
+ filename: `${this.streamUuid}.${this.extension}`,
272
+ bitDepth: this.bitDepth,
273
+ channels: this.recordingConfig?.channels ?? 1,
274
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
275
+ durationMs: this.currentDurationMs,
276
+ size: this.currentSize,
277
+ mimeType,
278
+ compression,
279
+ }
280
+ } catch (error) {
281
+ this.logger?.error('[Stop] Error stopping recording:', error)
282
+ throw error
283
+ }
220
284
  }
221
285
 
222
286
  // Pause recording
@@ -250,10 +314,19 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
250
314
  const status: AudioStreamStatus = {
251
315
  isRecording: this.isRecording,
252
316
  isPaused: this.isPaused,
253
- durationMs: Date.now() - this.recordingStartTime,
317
+ durationMs: this.currentDurationMs,
254
318
  size: this.currentSize,
255
319
  interval: this.currentInterval,
256
320
  mimeType: `audio/${this.extension}`,
321
+ compression: this.recordingConfig?.compression?.enabled
322
+ ? {
323
+ size: this.totalCompressedSize,
324
+ mimeType: 'audio/webm',
325
+ format: this.recordingConfig.compression.format ?? 'opus',
326
+ bitrate:
327
+ this.recordingConfig.compression.bitrate ?? 128000,
328
+ }
329
+ : undefined,
257
330
  }
258
331
  return status
259
332
  }