@siteed/expo-audio-stream 0.1.0 → 0.2.1
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/README.md +75 -6
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +129 -49
- package/build/ExpoAudioStream.types.d.ts +8 -3
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js +0 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStreamModule.js +2 -2
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/ExpoAudioStreamModule.web.d.ts +3 -3
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.web.js +27 -17
- package/build/ExpoAudioStreamModule.web.js.map +1 -1
- package/build/index.d.ts +4 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -11
- package/build/index.js.map +1 -1
- package/build/useAudioRecording.d.ts +4 -4
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +64 -65
- package/build/useAudioRecording.js.map +1 -1
- package/ios/AudioStreamManager.swift +189 -37
- package/ios/ExpoAudioStreamModule.swift +21 -12
- package/package.json +9 -3
- package/plugin/build/index.d.ts +1 -1
- package/plugin/build/index.js +4 -4
- package/plugin/src/index.ts +14 -8
- package/src/ExpoAudioStream.types.ts +20 -11
- package/src/ExpoAudioStreamModule.ts +2 -2
- package/src/ExpoAudioStreamModule.web.ts +165 -149
- package/src/index.ts +15 -13
- package/src/useAudioRecording.ts +146 -127
- package/yarn-error.log +0 -72
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# expo-audio-stream
|
|
1
|
+
# @siteed/expo-audio-stream
|
|
2
2
|
|
|
3
3
|
`@siteed/expo-audio-stream` is a comprehensive library designed to facilitate real-time audio processing and streaming across iOS, Android, and web platforms. This library leverages Expo's robust ecosystem to simplify the implementation of audio recording and streaming functionalities within React Native applications. Key features include audio streaming with configurable buffer intervals and automatic handling of microphone permissions in managed Expo projects.
|
|
4
4
|
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
- Real-time audio streaming across iOS, Android, and web.
|
|
8
8
|
- Configurable intervals for audio buffer receipt.
|
|
9
9
|
- Automated microphone permissions setup in managed Expo projects.
|
|
10
|
+
- IOS is automatically setup to handle background audio recording.
|
|
10
11
|
- Listeners for audio data events with detailed event payloads.
|
|
11
12
|
- Utility functions for recording control and file management.
|
|
12
13
|
|
|
13
|
-
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
To install `@siteed/expo-audio-stream`, add it to your project using npm or Yarn:
|
|
@@ -39,32 +39,39 @@ Add the plugin to your app.json like so:
|
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
42
|
+
The `example/` folder contains a fully functional React Native application that demonstrates how to integrate and use the `@siteed/expo-audio-stream` library in a real-world scenario. This sample application includes features such as starting and stopping audio recordings, handling permissions, and processing live audio data.
|
|
43
|
+
|
|
42
44
|
### Importing the module
|
|
43
45
|
|
|
44
46
|
```tsx
|
|
45
47
|
import {
|
|
46
48
|
useAudioRecorder,
|
|
49
|
+
AudioStreamResult,
|
|
47
50
|
} from 'expo-audio-stream';
|
|
48
51
|
|
|
49
52
|
export default function App() {
|
|
50
53
|
const { startRecording, stopRecording, duration, size, isRecording } = useAudioRecorder({
|
|
51
|
-
onAudioStream: (
|
|
52
|
-
console.log(`audio event
|
|
54
|
+
onAudioStream: (audioData: Blob) => {
|
|
55
|
+
console.log(`audio event`,audioData);
|
|
53
56
|
}
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
const handleStart = async () => {
|
|
57
60
|
const { granted } = await Audio.requestPermissionsAsync();
|
|
58
61
|
if (granted) {
|
|
59
|
-
startRecording({interval: 500});
|
|
62
|
+
const fileUri = await startRecording({interval: 500});
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
65
|
|
|
66
|
+
const handleStop = async () => {
|
|
67
|
+
const result: AudioStreamResult = await stopRecording();
|
|
68
|
+
};
|
|
69
|
+
|
|
63
70
|
const renderRecording = () => (
|
|
64
71
|
<View>
|
|
65
72
|
<Text>Duration: {duration} ms</Text>
|
|
66
73
|
<Text>Size: {size} bytes</Text>
|
|
67
|
-
<Button title="Stop Recording" onPress={
|
|
74
|
+
<Button title="Stop Recording" onPress={handleStop} />
|
|
68
75
|
</View>
|
|
69
76
|
);
|
|
70
77
|
|
|
@@ -83,3 +90,65 @@ export default function App() {
|
|
|
83
90
|
}
|
|
84
91
|
```
|
|
85
92
|
|
|
93
|
+
The library also exposes an `addAudioEventListener` function that provides an `AudioEventPayload` object that you can subscribe to:
|
|
94
|
+
```tsx
|
|
95
|
+
export interface AudioEventPayload {
|
|
96
|
+
encoded?: string,
|
|
97
|
+
buffer?: Blob,
|
|
98
|
+
fileUri: string,
|
|
99
|
+
from: number,
|
|
100
|
+
deltaSize: number,
|
|
101
|
+
totalSize: number,
|
|
102
|
+
mimeType: string;
|
|
103
|
+
streamUuid: string,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const subscribe = addAudioEventListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer}) => {
|
|
108
|
+
log(`Received audio event:`, {fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length})
|
|
109
|
+
if(deltaSize > 0) {
|
|
110
|
+
// Coming from native ( ios / android ) otherwise buffer is set
|
|
111
|
+
if(Platform.OS !== 'web') {
|
|
112
|
+
// Read the audio file as a base64 string for comparison
|
|
113
|
+
try {
|
|
114
|
+
// convert encoded string to binary data
|
|
115
|
+
const binaryData = atob(encoded);
|
|
116
|
+
const content = new Uint8Array(binaryData.length);
|
|
117
|
+
for (let i = 0; i < binaryData.length; i++) {
|
|
118
|
+
content[i] = binaryData.charCodeAt(i);
|
|
119
|
+
}
|
|
120
|
+
const audioBlob = new Blob([content], { type: mimeType });
|
|
121
|
+
console.info(`Received audio blob:`, audioBlob);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error reading audio file:', error);
|
|
124
|
+
}
|
|
125
|
+
} else if(buffer) {
|
|
126
|
+
// Coming from web
|
|
127
|
+
console.info(`Received audio buffer:`, buffer)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return () => subscribe.remove();
|
|
132
|
+
}, []);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Recording configuration
|
|
136
|
+
|
|
137
|
+
- on Android and IOS, audio is recorded in wav format, 16khz sample rate, 16 bit depth, 1 channel.
|
|
138
|
+
- on web, it usually records in opus but it depends on the browser configuration.
|
|
139
|
+
|
|
140
|
+
If you want to process the audio livestream directly, I recommend having another encoding step to align the audio format across platforms.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
### Debug Configuration
|
|
144
|
+
|
|
145
|
+
This library uses the npm `debug` package, to enable logging you can:
|
|
146
|
+
```
|
|
147
|
+
localStorage.debug = 'expo-audio-stream:*'
|
|
148
|
+
```
|
|
149
|
+
or set the DEBUG environment variable to `expo-audio-stream:*`
|
|
150
|
+
|
|
151
|
+
### TODO
|
|
152
|
+
this package is still in development, and there are a few things that need to be done:
|
|
153
|
+
- add multiple format for native audio stream (wav, mp3, opus)
|
|
154
|
+
|
|
@@ -22,13 +22,18 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
22
22
|
import java.io.File
|
|
23
23
|
import java.io.FileOutputStream
|
|
24
24
|
import java.io.IOException
|
|
25
|
+
import java.io.RandomAccessFile
|
|
26
|
+
|
|
25
27
|
const val AUDIO_EVENT_NAME = "AudioData"
|
|
28
|
+
const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
|
|
29
|
+
const val DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
|
|
30
|
+
const val DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
|
|
26
31
|
|
|
27
32
|
class ExpoAudioStreamModule() : Module() {
|
|
28
33
|
private var audioRecord: AudioRecord? = null
|
|
29
|
-
private var sampleRateInHz =
|
|
30
|
-
private var channelConfig =
|
|
31
|
-
private var audioFormat =
|
|
34
|
+
private var sampleRateInHz = DEFAULT_SAMPLE_RATE
|
|
35
|
+
private var channelConfig = DEFAULT_CHANNEL_CONFIG
|
|
36
|
+
private var audioFormat = DEFAULT_AUDIO_FORMAT
|
|
32
37
|
private var bufferSizeInBytes: Int = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
33
38
|
private var isRecording = AtomicBoolean(false)
|
|
34
39
|
private val isPaused = AtomicBoolean(false)
|
|
@@ -44,6 +49,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
44
49
|
private var lastPauseTime = 0L
|
|
45
50
|
private var pausedDuration = 0L
|
|
46
51
|
private var lastEmittedSize = 0L
|
|
52
|
+
private var mimeType = "audio/wav"
|
|
47
53
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
48
54
|
|
|
49
55
|
@SuppressLint("MissingPermission")
|
|
@@ -72,6 +78,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
72
78
|
"duration" to totalRecordedTime,
|
|
73
79
|
"isRecording" to isRecording.get(),
|
|
74
80
|
"isPaused" to isPaused.get(),
|
|
81
|
+
"mime" to mimeType,
|
|
75
82
|
"size" to totalDataSize,
|
|
76
83
|
"interval" to interval,
|
|
77
84
|
)
|
|
@@ -87,26 +94,62 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
AsyncFunction("pauseRecording") { promise: Promise ->
|
|
97
|
+
Log.d("AudioRecorderModule", "Pausing recording")
|
|
90
98
|
pauseRecording(promise)
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
AsyncFunction("stopRecording") { promise: Promise ->
|
|
102
|
+
Log.d("AudioRecorderModule", "Stopping recording")
|
|
94
103
|
stopRecording(promise)
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
// Method to write WAV file header
|
|
108
|
+
private fun writeWavHeader(out: FileOutputStream, channelConfig: Int, sampleRate: Int, audioFormat: Int) {
|
|
109
|
+
val channels = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
|
|
110
|
+
val bitDepth = if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) 16 else 8
|
|
111
|
+
|
|
112
|
+
val header = ByteArray(44)
|
|
113
|
+
val byteRate = sampleRate * channels * bitDepth / 8
|
|
114
|
+
val blockAlign = channels * bitDepth / 8
|
|
115
|
+
|
|
116
|
+
// RIFF/WAVE header
|
|
117
|
+
"RIFF".toByteArray().copyInto(header, 0)
|
|
118
|
+
header[4] = 0 // Size will be updated later
|
|
119
|
+
"WAVE".toByteArray().copyInto(header, 8)
|
|
120
|
+
"fmt ".toByteArray().copyInto(header, 12)
|
|
121
|
+
|
|
122
|
+
// 16 for PCM
|
|
123
|
+
header[16] = 16
|
|
124
|
+
header[20] = 1 // Audio format 1 for PCM
|
|
125
|
+
header[22] = channels.toByte()
|
|
126
|
+
header[24] = (sampleRate and 0xff).toByte()
|
|
127
|
+
header[25] = (sampleRate shr 8 and 0xff).toByte()
|
|
128
|
+
header[26] = (sampleRate shr 16 and 0xff).toByte()
|
|
129
|
+
header[27] = (sampleRate shr 24 and 0xff).toByte()
|
|
130
|
+
header[28] = (byteRate and 0xff).toByte()
|
|
131
|
+
header[29] = (byteRate shr 8 and 0xff).toByte()
|
|
132
|
+
header[30] = (byteRate shr 16 and 0xff).toByte()
|
|
133
|
+
header[31] = (byteRate shr 24 and 0xff).toByte()
|
|
134
|
+
header[32] = blockAlign.toByte()
|
|
135
|
+
header[34] = bitDepth.toByte()
|
|
136
|
+
"data".toByteArray().copyInto(header, 36)
|
|
137
|
+
|
|
138
|
+
out.write(header, 0, 44)
|
|
139
|
+
}
|
|
140
|
+
|
|
98
141
|
private fun configureRecording(params: Map<String, Any?>) {
|
|
99
|
-
sampleRateInHz = (params["sampleRate"] as? Int) ?:
|
|
100
|
-
channelConfig = (params["channelConfig"] as? Int) ?:
|
|
101
|
-
audioFormat = (params["audioFormat"] as? Int) ?:
|
|
142
|
+
sampleRateInHz = (params["sampleRate"] as? Int) ?: DEFAULT_SAMPLE_RATE
|
|
143
|
+
channelConfig = (params["channelConfig"] as? Int) ?: DEFAULT_CHANNEL_CONFIG
|
|
144
|
+
audioFormat = (params["audioFormat"] as? Int) ?: DEFAULT_AUDIO_FORMAT
|
|
102
145
|
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
103
146
|
}
|
|
104
147
|
|
|
105
148
|
private fun listAudioFiles(): List<String> {
|
|
106
149
|
val filesDir = appContext.reactContext?.filesDir
|
|
107
|
-
// Filter to include only .
|
|
150
|
+
// Filter to include only .wav files
|
|
108
151
|
val files = filesDir?.listFiles { file ->
|
|
109
|
-
file.isFile && file.name.endsWith(".
|
|
152
|
+
file.isFile && file.name.endsWith(".wav")
|
|
110
153
|
}?.map { it.absolutePath } ?: listOf() // Use `listOf()` to return an empty list if null
|
|
111
154
|
return files
|
|
112
155
|
}
|
|
@@ -143,7 +186,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
143
186
|
// Log for new recording or resuming
|
|
144
187
|
if (!isPaused.get()) {
|
|
145
188
|
streamUuid = java.util.UUID.randomUUID().toString()
|
|
146
|
-
audioFile = File(appContext.reactContext?.filesDir, "audio_${streamUuid}.
|
|
189
|
+
audioFile = File(appContext.reactContext?.filesDir, "audio_${streamUuid}.wav")
|
|
147
190
|
Log.i("AudioRecorderModule", "Starting new recording $streamUuid with sample rate: $sampleRateInHz, channel config: $channelConfig, audio format: $audioFormat, buffer size: $bufferSizeInBytes, interval: $interval")
|
|
148
191
|
|
|
149
192
|
} else {
|
|
@@ -156,6 +199,15 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
156
199
|
return
|
|
157
200
|
}
|
|
158
201
|
|
|
202
|
+
try {
|
|
203
|
+
FileOutputStream(audioFile, true).use { fos ->
|
|
204
|
+
writeWavHeader(fos, channelConfig, sampleRateInHz, audioFormat)
|
|
205
|
+
}
|
|
206
|
+
} catch (e: IOException) {
|
|
207
|
+
promise.reject("FILE_CREATION_FAILED", "Failed to create audio file with WAV header", null)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
159
211
|
audioRecord?.startRecording()
|
|
160
212
|
isPaused.set(false)
|
|
161
213
|
isRecording.set(true)
|
|
@@ -166,7 +218,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
166
218
|
}
|
|
167
219
|
|
|
168
220
|
recordingThread = Thread { recordingProcess() }.apply { start() }
|
|
169
|
-
promise.resolve(
|
|
221
|
+
promise.resolve(audioFile?.toURI().toString())
|
|
170
222
|
}
|
|
171
223
|
|
|
172
224
|
private fun stopRecording(promise: Promise) {
|
|
@@ -182,7 +234,19 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
182
234
|
totalRecordedTime += (endTime - recordingStartTime - pausedDuration) // Adjust the total recording time
|
|
183
235
|
isRecording.set(false)
|
|
184
236
|
isPaused.set(false)
|
|
185
|
-
|
|
237
|
+
|
|
238
|
+
// Calculate the file size
|
|
239
|
+
val fileSize = audioFile?.length() ?: 0
|
|
240
|
+
|
|
241
|
+
// Create result bundle
|
|
242
|
+
val result = bundleOf(
|
|
243
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
244
|
+
"duration" to totalRecordedTime,
|
|
245
|
+
"size" to fileSize,
|
|
246
|
+
"mimeType" to mimeType
|
|
247
|
+
)
|
|
248
|
+
promise.resolve(result)
|
|
249
|
+
|
|
186
250
|
// Reset the timing variables
|
|
187
251
|
totalRecordedTime = 0
|
|
188
252
|
pausedDuration = 0
|
|
@@ -194,30 +258,49 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
194
258
|
}
|
|
195
259
|
|
|
196
260
|
private fun recordingProcess() {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
FileOutputStream(audioFile, true).use { fos ->
|
|
262
|
+
// Write audio data directly to the file
|
|
263
|
+
val audioData = ByteArray(bufferSizeInBytes)
|
|
264
|
+
while (isRecording.get()) {
|
|
265
|
+
if (!isPaused.get()) {
|
|
266
|
+
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
267
|
+
if (bytesRead < 0) {
|
|
268
|
+
Log.e("AudioRecorderModule", "Read error: $bytesRead")
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
if (bytesRead > 0) {
|
|
272
|
+
fos.write(audioData, 0, bytesRead)
|
|
273
|
+
totalDataSize += bytesRead
|
|
274
|
+
|
|
275
|
+
// Emit audio data at defined intervals
|
|
276
|
+
if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
|
|
277
|
+
emitAudioData(audioData, bytesRead)
|
|
278
|
+
lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
|
|
279
|
+
}
|
|
212
280
|
}
|
|
213
281
|
}
|
|
214
282
|
}
|
|
215
283
|
}
|
|
216
|
-
|
|
217
|
-
emitAudioData() // Emit any remaining data
|
|
218
|
-
}
|
|
284
|
+
updateWavHeader() // Update the header with the correct file size after recording stops
|
|
219
285
|
}
|
|
220
286
|
|
|
287
|
+
private fun updateWavHeader() {
|
|
288
|
+
try {
|
|
289
|
+
RandomAccessFile(audioFile, "rw").use { raf ->
|
|
290
|
+
val fileSize = raf.length()
|
|
291
|
+
val dataSize = fileSize - 44 // Subtract the header size
|
|
292
|
+
raf.seek(4) // Skip 'RIFF' label
|
|
293
|
+
|
|
294
|
+
// Write correct file size, excluding the first 8 bytes of the RIFF header
|
|
295
|
+
raf.writeInt(Integer.reverseBytes((dataSize + 36).toInt()))
|
|
296
|
+
|
|
297
|
+
raf.seek(40) // Go to the data size position
|
|
298
|
+
raf.writeInt(Integer.reverseBytes(dataSize.toInt())) // Write the size of the data segment
|
|
299
|
+
}
|
|
300
|
+
} catch (e: IOException) {
|
|
301
|
+
Log.e("AudioRecorderModule", "Could not update WAV header", e)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
221
304
|
|
|
222
305
|
private fun clearAudioStorage() {
|
|
223
306
|
// Clear all files in the app's internal storage
|
|
@@ -228,34 +311,31 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
228
311
|
}
|
|
229
312
|
}
|
|
230
313
|
|
|
231
|
-
private fun emitAudioData() {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
val encodedBuffer = encodeAudioData(rawData)
|
|
238
|
-
val fileSize = audioFile?.length() ?: 0
|
|
314
|
+
private fun emitAudioData(audioData: ByteArray, length: Int) {
|
|
315
|
+
// Since audioData now contains the actual bytes read, you do not need to read from audioDataBuffer.
|
|
316
|
+
// Encode the part of the buffer that contains new audio data.
|
|
317
|
+
val encodedBuffer = encodeAudioData(audioData)
|
|
318
|
+
val fileSize = audioFile?.length() ?: 0 // Update file size information
|
|
239
319
|
val from = lastEmittedSize
|
|
240
320
|
val deltaSize = fileSize - lastEmittedSize
|
|
241
|
-
lastEmittedSize = fileSize
|
|
321
|
+
lastEmittedSize = fileSize // Update last emitted size
|
|
322
|
+
|
|
242
323
|
mainHandler.post {
|
|
243
324
|
try {
|
|
244
|
-
this@ExpoAudioStreamModule.sendEvent(AUDIO_EVENT_NAME,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
325
|
+
this@ExpoAudioStreamModule.sendEvent(AUDIO_EVENT_NAME, bundleOf(
|
|
326
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
327
|
+
"from" to from,
|
|
328
|
+
"encoded" to encodedBuffer,
|
|
329
|
+
"deltaSize" to deltaSize,
|
|
330
|
+
"mimeType" to mimeType,
|
|
331
|
+
"totalSize" to fileSize,
|
|
332
|
+
"streamUuid" to streamUuid
|
|
333
|
+
))
|
|
254
334
|
} catch (e: Exception) {
|
|
255
335
|
Log.e("AudioRecorderModule", "Failed to send event", e)
|
|
256
336
|
}
|
|
257
337
|
}
|
|
258
|
-
audioDataBuffer.reset()
|
|
338
|
+
// audioDataBuffer.reset() is no longer needed here as we do not use the buffer to store ongoing data
|
|
259
339
|
}
|
|
260
340
|
|
|
261
341
|
private fun encodeAudioData(rawData: ByteArray): String {
|
|
@@ -5,19 +5,24 @@ export interface AudioEventPayload {
|
|
|
5
5
|
from: number;
|
|
6
6
|
deltaSize: number;
|
|
7
7
|
totalSize: number;
|
|
8
|
+
mimeType: string;
|
|
8
9
|
streamUuid: string;
|
|
9
10
|
}
|
|
11
|
+
export interface AudioStreamResult {
|
|
12
|
+
fileUri: string;
|
|
13
|
+
duration: number;
|
|
14
|
+
size: number;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
}
|
|
10
17
|
export interface AudioStreamStatus {
|
|
11
18
|
isRecording: boolean;
|
|
12
19
|
isPaused: boolean;
|
|
13
20
|
duration: number;
|
|
14
21
|
size: number;
|
|
15
22
|
interval: number;
|
|
23
|
+
mimeType: string;
|
|
16
24
|
}
|
|
17
25
|
export interface RecordingOptions {
|
|
18
|
-
sampleRate?: number;
|
|
19
|
-
channelConfig?: number;
|
|
20
|
-
audioFormat?: number;
|
|
21
26
|
interval?: number;
|
|
22
27
|
}
|
|
23
28
|
//# sourceMappingURL=ExpoAudioStream.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAK/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface AudioEventPayload {\n encoded?: string;\n buffer?: Blob;\n fileUri: string;\n from: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n}\n\nexport interface AudioStreamResult {\n fileUri: string;\n duration: number;\n size: number;\n mimeType: string;\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\n mimeType: string;\n}\n\nexport interface RecordingOptions {\n // TODO align Android and IOS options\n // sampleRate?: number;\n // channelConfig?: number; // numberOfChannel\n // audioFormat?: number; // bitDepth (ENCODING_PCM_16BIT --> 2)\n interval?: number;\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { requireNativeModule } from
|
|
1
|
+
import { requireNativeModule } from "expo-modules-core";
|
|
2
2
|
// It loads the native module object from the JSI or falls back to
|
|
3
3
|
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
|
4
|
-
export default requireNativeModule(
|
|
4
|
+
export default requireNativeModule("ExpoAudioStream");
|
|
5
5
|
//# sourceMappingURL=ExpoAudioStreamModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStreamModule.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,kEAAkE;AAClE,4EAA4E;AAC5E,eAAe,mBAAmB,CAAC,iBAAiB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,kEAAkE;AAClE,4EAA4E;AAC5E,eAAe,mBAAmB,CAAC,iBAAiB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from \"expo-modules-core\";\n\n// It loads the native module object from the JSI or falls back to\n// the bridge module (from NativeModulesProxy) if the remote debugger is on.\nexport default requireNativeModule(\"ExpoAudioStream\");\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "expo-modules-core";
|
|
2
|
-
import { RecordingOptions } from "./ExpoAudioStream.types";
|
|
2
|
+
import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
|
|
3
3
|
declare class ExpoAudioStreamWeb extends EventEmitter {
|
|
4
4
|
mediaRecorder: MediaRecorder | null;
|
|
5
5
|
audioChunks: Blob[];
|
|
@@ -14,11 +14,11 @@ declare class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
14
14
|
streamUuid: string | null;
|
|
15
15
|
constructor();
|
|
16
16
|
getMediaStream(): Promise<MediaStream>;
|
|
17
|
-
startRecording(options?: RecordingOptions): Promise<
|
|
17
|
+
startRecording(options?: RecordingOptions): Promise<string>;
|
|
18
18
|
setupRecordingListeners(): void;
|
|
19
19
|
emitAudioEvent(data: Blob): void;
|
|
20
20
|
generateUUID(): string;
|
|
21
|
-
stopRecording(): Promise<
|
|
21
|
+
stopRecording(): Promise<AudioStreamResult | null>;
|
|
22
22
|
pauseRecording(): Promise<void>;
|
|
23
23
|
status(): {
|
|
24
24
|
isRecording: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAEL,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAGjC,cAAM,kBAAmB,SAAQ,YAAY;IAC3C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;;IA2BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,gBAAqB;IAmBnD,uBAAuB;IA0BvB,cAAc,CAAC,IAAI,EAAE,IAAI;IAgBzB,YAAY;IAUN,aAAa,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAelD,cAAc;IAcpB,MAAM;;;;;;;IAUN,cAAc;IAId,eAAe;CAGhB;;AAED,wBAAwC"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import debug from "debug";
|
|
1
2
|
import { EventEmitter } from "expo-modules-core";
|
|
3
|
+
const log = debug("expo-audio-stream:useAudioRecording");
|
|
2
4
|
class ExpoAudioStreamWeb extends EventEmitter {
|
|
3
5
|
mediaRecorder;
|
|
4
6
|
audioChunks;
|
|
@@ -14,11 +16,11 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
14
16
|
constructor() {
|
|
15
17
|
const mockNativeModule = {
|
|
16
18
|
addListener: (eventName) => {
|
|
17
|
-
|
|
19
|
+
// Not used on web
|
|
18
20
|
},
|
|
19
21
|
removeListeners: (count) => {
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
+
// Not used on web
|
|
23
|
+
},
|
|
22
24
|
};
|
|
23
25
|
super(mockNativeModule); // Pass the mock native module to the parent class
|
|
24
26
|
this.mediaRecorder = null;
|
|
@@ -46,7 +48,7 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
46
48
|
// Start recording with options
|
|
47
49
|
async startRecording(options = {}) {
|
|
48
50
|
if (this.isRecording) {
|
|
49
|
-
throw new Error(
|
|
51
|
+
throw new Error("Recording is already in progress");
|
|
50
52
|
}
|
|
51
53
|
const stream = await this.getMediaStream();
|
|
52
54
|
this.mediaRecorder = new MediaRecorder(stream);
|
|
@@ -57,11 +59,13 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
57
59
|
this.pausedTime = 0;
|
|
58
60
|
this.lastEmittedSize = 0;
|
|
59
61
|
this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
|
|
62
|
+
const fileUri = `${this.streamUuid}.webm`;
|
|
63
|
+
return fileUri;
|
|
60
64
|
}
|
|
61
65
|
// Setup listeners for the MediaRecorder
|
|
62
66
|
setupRecordingListeners() {
|
|
63
67
|
if (!this.mediaRecorder) {
|
|
64
|
-
throw new Error(
|
|
68
|
+
throw new Error("No active media recorder");
|
|
65
69
|
}
|
|
66
70
|
this.mediaRecorder.ondataavailable = (event) => {
|
|
67
71
|
this.audioChunks.push(event.data);
|
|
@@ -71,55 +75,61 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
71
75
|
};
|
|
72
76
|
this.mediaRecorder.onstop = () => {
|
|
73
77
|
this.isRecording = false;
|
|
74
|
-
|
|
78
|
+
log("Recording stopped", this.audioChunks);
|
|
75
79
|
};
|
|
76
80
|
this.mediaRecorder.onpause = () => {
|
|
77
81
|
this.isPaused = true;
|
|
78
82
|
};
|
|
79
83
|
this.mediaRecorder.onresume = () => {
|
|
80
84
|
this.isPaused = false;
|
|
81
|
-
this.recordingStartTime +=
|
|
85
|
+
this.recordingStartTime += Date.now() - this.pausedTime; // Adjust start time after resuming
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
emitAudioEvent(data) {
|
|
85
|
-
const fileUri = `${this.streamUuid}.
|
|
89
|
+
const fileUri = `${this.streamUuid}.webm`;
|
|
86
90
|
const audioEventPayload = {
|
|
87
|
-
fileUri
|
|
91
|
+
fileUri,
|
|
92
|
+
mimeType: "audio/webm",
|
|
88
93
|
from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
|
|
89
94
|
deltaSize: data.size,
|
|
90
95
|
totalSize: this.currentSize,
|
|
91
96
|
buffer: data,
|
|
92
|
-
streamUuid: this.streamUuid ??
|
|
97
|
+
streamUuid: this.streamUuid ?? "", // Generate or manage UUID for stream identification
|
|
93
98
|
};
|
|
94
|
-
this.emit(
|
|
99
|
+
this.emit("AudioData", audioEventPayload);
|
|
95
100
|
}
|
|
96
101
|
// Helper method to generate a UUID
|
|
97
102
|
generateUUID() {
|
|
98
103
|
// Implementation of UUID generation (use a library or custom method)
|
|
99
|
-
return
|
|
100
|
-
const r = Math.random() * 16 | 0, v = c
|
|
104
|
+
return "xxxx-xxxx-xxxx-xxxx".replace(/[x]/g, (c) => {
|
|
105
|
+
const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
101
106
|
return v.toString(16);
|
|
102
107
|
});
|
|
103
108
|
}
|
|
104
109
|
// Stop recording
|
|
105
110
|
async stopRecording() {
|
|
106
|
-
console.debug('Stopping recording', this);
|
|
107
111
|
this.mediaRecorder?.stop();
|
|
108
112
|
this.isRecording = false;
|
|
109
113
|
this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;
|
|
110
|
-
|
|
114
|
+
const result = {
|
|
115
|
+
fileUri: `${this.streamUuid}.webm`,
|
|
116
|
+
duration: this.currentDuration,
|
|
117
|
+
size: this.currentSize,
|
|
118
|
+
mimeType: "audio/webm",
|
|
119
|
+
};
|
|
120
|
+
return result;
|
|
111
121
|
}
|
|
112
122
|
// Pause recording
|
|
113
123
|
async pauseRecording() {
|
|
114
124
|
if (!this.mediaRecorder) {
|
|
115
|
-
throw new Error(
|
|
125
|
+
throw new Error("No active media recorder");
|
|
116
126
|
}
|
|
117
127
|
if (this.isRecording && !this.isPaused) {
|
|
118
128
|
this.mediaRecorder.pause();
|
|
119
129
|
this.pausedTime = Date.now();
|
|
120
130
|
}
|
|
121
131
|
else {
|
|
122
|
-
throw new Error(
|
|
132
|
+
throw new Error("Recording is not active or already paused");
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
// Get current status
|