@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.
- package/CHANGELOG.md +34 -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 +22 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +15 -2
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +99 -40
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/WebRecorder.web.d.ts +14 -3
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +188 -100
- 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 +2 -1
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +46 -5
- package/build/useAudioRecorder.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +65 -160
- package/build/workers/inlineAudioWebWorker.web.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 +24 -1
- package/src/ExpoAudioStream.web.ts +111 -38
- package/src/WebRecorder.web.ts +238 -138
- package/src/events.ts +7 -0
- package/src/useAudioRecorder.tsx +68 -7
- 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
|
-
|
|
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,
|
|
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 ?? '',
|
|
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
|
-
|
|
232
|
+
this.logger?.debug('[Stop] Starting stop process')
|
|
233
|
+
const startTime = performance.now()
|
|
184
234
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
199
|
-
|
|
243
|
+
let compression: AudioRecording['compression']
|
|
244
|
+
let fileUri = `${this.streamUuid}.${this.extension}`
|
|
245
|
+
let mimeType = `audio/${this.extension}`
|
|
200
246
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|