@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 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: (base64Data) => {
52
- console.log(`audio event ${typeof base64Data}`, base64Data);
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={stopRecording} />
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 = 44100 // Default sample rate
30
- private var channelConfig = AudioFormat.CHANNEL_IN_MONO
31
- private var audioFormat = AudioFormat.ENCODING_PCM_16BIT
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) ?: 44100
100
- channelConfig = (params["channelConfig"] as? Int) ?: AudioFormat.CHANNEL_IN_MONO
101
- audioFormat = (params["audioFormat"] as? Int) ?: AudioFormat.ENCODING_PCM_16BIT
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 .pcm files
150
+ // Filter to include only .wav files
108
151
  val files = filesDir?.listFiles { file ->
109
- file.isFile && file.name.endsWith(".pcm")
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}.pcm")
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(null)
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
- promise.resolve(totalRecordedTime)
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
- val audioData = ByteArray(bufferSizeInBytes)
199
- while (isRecording.get()) {
200
- if (!isPaused.get()) {
201
- val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
202
- if (bytesRead < 0) {
203
- Log.e("AudioRecorderModule", "Read error: $bytesRead")
204
- break
205
- }
206
- if (bytesRead > 0) {
207
- audioDataBuffer.write(audioData, 0, bytesRead)
208
- totalDataSize += bytesRead
209
- if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
210
- emitAudioData()
211
- lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
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
- if (audioDataBuffer.size() > 0) {
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
- val rawData = audioDataBuffer.toByteArray()
233
- if (!saveAudioToFile(rawData)) {
234
- Log.e("AudioRecorderModule", "Failed to save audio data")
235
- return
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
- bundleOf(
246
- "fileUri" to audioFile?.toURI().toString(),
247
- "from" to from,
248
- "encoded" to encodedBuffer,
249
- "deltaSize" to deltaSize,
250
- "totalSize" to fileSize,
251
- "streamUuid" to streamUuid
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() // Clear the buffer after emitting
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,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAE/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
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,3 +1,2 @@
1
- ;
2
1
  export {};
3
2
  //# sourceMappingURL=ExpoAudioStream.types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAQC,CAAC","sourcesContent":["export interface AudioEventPayload {\n encoded?: string, \n buffer?: Blob,\n fileUri: string,\n from: number,\n deltaSize: number,\n totalSize: number,\n streamUuid: string,\n};\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\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
+ {"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 'expo-modules-core';
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('ExpoAudioStream');
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 '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
+ {"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<void>;
17
+ startRecording(options?: RecordingOptions): Promise<string>;
18
18
  setupRecordingListeners(): void;
19
19
  emitAudioEvent(data: Blob): void;
20
20
  generateUUID(): string;
21
- stopRecording(): Promise<number>;
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":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAqB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE9E,cAAM,kBAAmB,SAAQ,YAAY;IACzC,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;;IA4BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,gBAAqB;IAiBnD,uBAAuB;IA0BvB,cAAc,CAAC,IAAI,EAAE,IAAI;IAexB,YAAY;IASP,aAAa;IASb,cAAc;IAcpB,MAAM;;;;;;;IAUN,cAAc;IAId,eAAe;CAGlB;;AAED,wBAAwC"}
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
- console.log(`Web addListener called for ${eventName}`);
19
+ // Not used on web
18
20
  },
19
21
  removeListeners: (count) => {
20
- console.log(`Web removeListeners called, count: ${count}`);
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('Recording is already in progress');
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('No active media recorder');
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
- console.log('Recording stopped', this.audioChunks);
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 += (Date.now() - this.pausedTime); // Adjust start time after resuming
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}.pcm`;
89
+ const fileUri = `${this.streamUuid}.webm`;
86
90
  const audioEventPayload = {
87
- fileUri: 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 ?? '', // Generate or manage UUID for stream identification
97
+ streamUuid: this.streamUuid ?? "", // Generate or manage UUID for stream identification
93
98
  };
94
- this.emit('AudioData', audioEventPayload);
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 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {
100
- const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
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
- return this.currentDuration;
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('No active media recorder');
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('Recording is not active or already paused');
132
+ throw new Error("Recording is not active or already paused");
123
133
  }
124
134
  }
125
135
  // Get current status