@siteed/expo-audio-stream 0.1.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/.eslintrc.js +2 -0
- package/README.md +85 -0
- package/android/.gradle/8.1.1/checksums/checksums.lock +0 -0
- package/android/.gradle/8.1.1/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.1.1/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.1.1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.1.1/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.1.1/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +92 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +318 -0
- package/app.plugin.js +1 -0
- package/build/ExpoAudioStream.types.d.ts +23 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -0
- package/build/ExpoAudioStream.types.js +3 -0
- package/build/ExpoAudioStream.types.js.map +1 -0
- package/build/ExpoAudioStreamModule.d.ts +3 -0
- package/build/ExpoAudioStreamModule.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.js +5 -0
- package/build/ExpoAudioStreamModule.js.map +1 -0
- package/build/ExpoAudioStreamModule.web.d.ts +35 -0
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.web.js +143 -0
- package/build/ExpoAudioStreamModule.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/useAudioRecording.d.ts +15 -0
- package/build/useAudioRecording.d.ts.map +1 -0
- package/build/useAudioRecording.js +118 -0
- package/build/useAudioRecording.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioStreamManager.swift +168 -0
- package/ios/ExpoAudioStream.podspec +27 -0
- package/ios/ExpoAudioStreamModule.swift +133 -0
- package/package.json +61 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +17 -0
- package/plugin/src/index.ts +27 -0
- package/plugin/tsconfig.json +9 -0
- package/release-it.js +18 -0
- package/src/ExpoAudioStream.types.ts +25 -0
- package/src/ExpoAudioStreamModule.ts +5 -0
- package/src/ExpoAudioStreamModule.web.ts +162 -0
- package/src/index.ts +29 -0
- package/src/useAudioRecording.ts +141 -0
- package/tsconfig.json +9 -0
- package/yarn-error.log +72 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# expo-audio-stream
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Real-time audio streaming across iOS, Android, and web.
|
|
8
|
+
- Configurable intervals for audio buffer receipt.
|
|
9
|
+
- Automated microphone permissions setup in managed Expo projects.
|
|
10
|
+
- Listeners for audio data events with detailed event payloads.
|
|
11
|
+
- Utility functions for recording control and file management.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
To install `@siteed/expo-audio-stream`, add it to your project using npm or Yarn:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @siteed/expo-audio-stream
|
|
20
|
+
# or
|
|
21
|
+
yarn add @siteed/expo-audio-stream
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Make sure that you have Expo set up in your project. For details on setting up Expo, refer to the Expo documentation.
|
|
25
|
+
|
|
26
|
+
### Configuring with app.json
|
|
27
|
+
|
|
28
|
+
To ensure expo-audio-stream works correctly with Expo, you must add it as a plugin in your app.json configuration file. This step is crucial as it allows Expo to load any necessary configurations or permissions required by the library.
|
|
29
|
+
|
|
30
|
+
Add the plugin to your app.json like so:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"expo": {
|
|
35
|
+
"plugins": ["@siteed/expo-audio-stream"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Importing the module
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import {
|
|
46
|
+
useAudioRecorder,
|
|
47
|
+
} from 'expo-audio-stream';
|
|
48
|
+
|
|
49
|
+
export default function App() {
|
|
50
|
+
const { startRecording, stopRecording, duration, size, isRecording } = useAudioRecorder({
|
|
51
|
+
onAudioStream: (base64Data) => {
|
|
52
|
+
console.log(`audio event ${typeof base64Data}`, base64Data);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const handleStart = async () => {
|
|
57
|
+
const { granted } = await Audio.requestPermissionsAsync();
|
|
58
|
+
if (granted) {
|
|
59
|
+
startRecording({interval: 500});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const renderRecording = () => (
|
|
64
|
+
<View>
|
|
65
|
+
<Text>Duration: {duration} ms</Text>
|
|
66
|
+
<Text>Size: {size} bytes</Text>
|
|
67
|
+
<Button title="Stop Recording" onPress={stopRecording} />
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const renderStopped = () => (
|
|
72
|
+
<View>
|
|
73
|
+
<Button title="Start Recording" onPress={handleStart} />
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<View>
|
|
79
|
+
<Button title="Request Permission" onPress={() => Audio.requestPermissionsAsync()} />
|
|
80
|
+
{isRecording ? renderRecording() : renderStopped()}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
group = 'net.siteed.audiostream'
|
|
6
|
+
version = '0.1.0'
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
|
11
|
+
apply from: expoModulesCorePlugin
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
16
|
+
ext.safeExtGet = { prop, fallback ->
|
|
17
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Ensures backward compatibility
|
|
21
|
+
ext.getKotlinVersion = {
|
|
22
|
+
if (ext.has("kotlinVersion")) {
|
|
23
|
+
ext.kotlinVersion()
|
|
24
|
+
} else {
|
|
25
|
+
ext.safeExtGet("kotlinVersion", "1.8.10")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
repositories {
|
|
30
|
+
mavenCentral()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dependencies {
|
|
34
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEvaluate {
|
|
39
|
+
publishing {
|
|
40
|
+
publications {
|
|
41
|
+
release(MavenPublication) {
|
|
42
|
+
from components.release
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
repositories {
|
|
46
|
+
maven {
|
|
47
|
+
url = mavenLocal().url
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
android {
|
|
54
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
|
55
|
+
|
|
56
|
+
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
|
|
57
|
+
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
|
|
58
|
+
compileOptions {
|
|
59
|
+
sourceCompatibility JavaVersion.VERSION_11
|
|
60
|
+
targetCompatibility JavaVersion.VERSION_11
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
kotlinOptions {
|
|
64
|
+
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
namespace "net.siteed.audiostream"
|
|
69
|
+
defaultConfig {
|
|
70
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
71
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
72
|
+
versionCode 1
|
|
73
|
+
versionName "0.1.0"
|
|
74
|
+
}
|
|
75
|
+
lintOptions {
|
|
76
|
+
abortOnError false
|
|
77
|
+
}
|
|
78
|
+
publishing {
|
|
79
|
+
singleVariant("release") {
|
|
80
|
+
withSourcesJar()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
repositories {
|
|
86
|
+
mavenCentral()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
dependencies {
|
|
90
|
+
implementation project(':expo-modules-core')
|
|
91
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
92
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.media.AudioFormat
|
|
7
|
+
import android.media.AudioRecord
|
|
8
|
+
import android.media.MediaRecorder
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.core.os.bundleOf
|
|
12
|
+
import expo.modules.kotlin.modules.Module
|
|
13
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
14
|
+
import expo.modules.kotlin.Promise
|
|
15
|
+
import android.util.Base64
|
|
16
|
+
import android.os.Handler
|
|
17
|
+
import android.os.SystemClock
|
|
18
|
+
import java.io.ByteArrayOutputStream
|
|
19
|
+
import android.os.Looper
|
|
20
|
+
import expo.modules.core.interfaces.Function
|
|
21
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
22
|
+
import java.io.File
|
|
23
|
+
import java.io.FileOutputStream
|
|
24
|
+
import java.io.IOException
|
|
25
|
+
const val AUDIO_EVENT_NAME = "AudioData"
|
|
26
|
+
|
|
27
|
+
class ExpoAudioStreamModule() : Module() {
|
|
28
|
+
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
|
|
32
|
+
private var bufferSizeInBytes: Int = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
33
|
+
private var isRecording = AtomicBoolean(false)
|
|
34
|
+
private val isPaused = AtomicBoolean(false)
|
|
35
|
+
private var streamUuid: String? = null
|
|
36
|
+
private var audioFile: File? = null
|
|
37
|
+
private var recordingThread: Thread? = null
|
|
38
|
+
private var recordingStartTime: Long = 0
|
|
39
|
+
private var totalRecordedTime: Long = 0
|
|
40
|
+
private var totalDataSize = 0
|
|
41
|
+
private val audioDataBuffer = ByteArrayOutputStream()
|
|
42
|
+
private var interval = 1000L // Emit data every 1000 milliseconds (1 second)
|
|
43
|
+
private var lastEmitTime = SystemClock.elapsedRealtime()
|
|
44
|
+
private var lastPauseTime = 0L
|
|
45
|
+
private var pausedDuration = 0L
|
|
46
|
+
private var lastEmittedSize = 0L
|
|
47
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
48
|
+
|
|
49
|
+
@SuppressLint("MissingPermission")
|
|
50
|
+
override fun definition() = ModuleDefinition {
|
|
51
|
+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
|
52
|
+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
|
53
|
+
// The module will be accessible from `requireNativeModule('ExpoAudioStream')` in JavaScript.
|
|
54
|
+
Name("ExpoAudioStream")
|
|
55
|
+
|
|
56
|
+
Events(AUDIO_EVENT_NAME)
|
|
57
|
+
|
|
58
|
+
AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
59
|
+
configureRecording(options)
|
|
60
|
+
startRecording(options, promise)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Function("clearAudioFiles") {
|
|
64
|
+
clearAudioStorage()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Function("status") {
|
|
68
|
+
val currentTime = System.currentTimeMillis()
|
|
69
|
+
totalRecordedTime = (currentTime - recordingStartTime - pausedDuration) // Adjust the total recording time
|
|
70
|
+
|
|
71
|
+
bundleOf(
|
|
72
|
+
"duration" to totalRecordedTime,
|
|
73
|
+
"isRecording" to isRecording.get(),
|
|
74
|
+
"isPaused" to isPaused.get(),
|
|
75
|
+
"size" to totalDataSize,
|
|
76
|
+
"interval" to interval,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
AsyncFunction("listAudioFiles") { promise: Promise ->
|
|
81
|
+
try {
|
|
82
|
+
val fileList = listAudioFiles()
|
|
83
|
+
promise.resolve(fileList)
|
|
84
|
+
} catch (e: Exception) {
|
|
85
|
+
promise.reject("ERROR_LIST_FILES", "Failed to list audio files", e)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
AsyncFunction("pauseRecording") { promise: Promise ->
|
|
90
|
+
pauseRecording(promise)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
AsyncFunction("stopRecording") { promise: Promise ->
|
|
94
|
+
stopRecording(promise)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
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
|
|
102
|
+
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun listAudioFiles(): List<String> {
|
|
106
|
+
val filesDir = appContext.reactContext?.filesDir
|
|
107
|
+
// Filter to include only .pcm files
|
|
108
|
+
val files = filesDir?.listFiles { file ->
|
|
109
|
+
file.isFile && file.name.endsWith(".pcm")
|
|
110
|
+
}?.map { it.absolutePath } ?: listOf() // Use `listOf()` to return an empty list if null
|
|
111
|
+
return files
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
private fun startRecording(options: Map<String, Any?>, promise: Promise) {
|
|
116
|
+
if (!checkPermission()) {
|
|
117
|
+
promise.reject("PERMISSION_DENIED", "Recording permission has not been granted", null)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
122
|
+
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
val intervalOption = options["interval"]
|
|
127
|
+
if (intervalOption != null) {
|
|
128
|
+
Log.d("AudioRecorderModule", "Setting interval to $intervalOption")
|
|
129
|
+
if (intervalOption is Number) {
|
|
130
|
+
val intervalValue = intervalOption.toLong()
|
|
131
|
+
if (intervalValue < 100) {
|
|
132
|
+
promise.reject("INVALID_INTERVAL", "Interval must be at least 100 ms", null)
|
|
133
|
+
return
|
|
134
|
+
} else {
|
|
135
|
+
this.interval = intervalValue
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
promise.reject("INVALID_INTERVAL", "Interval must be a number", null)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Log for new recording or resuming
|
|
144
|
+
if (!isPaused.get()) {
|
|
145
|
+
streamUuid = java.util.UUID.randomUUID().toString()
|
|
146
|
+
audioFile = File(appContext.reactContext?.filesDir, "audio_${streamUuid}.pcm")
|
|
147
|
+
Log.i("AudioRecorderModule", "Starting new recording $streamUuid with sample rate: $sampleRateInHz, channel config: $channelConfig, audio format: $audioFormat, buffer size: $bufferSizeInBytes, interval: $interval")
|
|
148
|
+
|
|
149
|
+
} else {
|
|
150
|
+
Log.i("AudioRecorderModule", "Resuming recording")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Initialize the recorder if it's a new recording
|
|
154
|
+
if (!isPaused.get() && !initializeRecorder()) {
|
|
155
|
+
promise.reject("INITIALIZATION_FAILED", "AudioRecord initialization failed", null)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
audioRecord?.startRecording()
|
|
160
|
+
isPaused.set(false)
|
|
161
|
+
isRecording.set(true)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if (!isPaused.get()) {
|
|
165
|
+
recordingStartTime = System.currentTimeMillis() // Only reset start time if it's not a resume
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
recordingThread = Thread { recordingProcess() }.apply { start() }
|
|
169
|
+
promise.resolve(null)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun stopRecording(promise: Promise) {
|
|
173
|
+
if (!isRecording.get()) {
|
|
174
|
+
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
audioRecord?.stop()
|
|
180
|
+
audioRecord?.release()
|
|
181
|
+
val endTime = System.currentTimeMillis()
|
|
182
|
+
totalRecordedTime += (endTime - recordingStartTime - pausedDuration) // Adjust the total recording time
|
|
183
|
+
isRecording.set(false)
|
|
184
|
+
isPaused.set(false)
|
|
185
|
+
promise.resolve(totalRecordedTime)
|
|
186
|
+
// Reset the timing variables
|
|
187
|
+
totalRecordedTime = 0
|
|
188
|
+
pausedDuration = 0
|
|
189
|
+
} catch (e: Exception) {
|
|
190
|
+
promise.reject("STOP_FAILED", "Failed to stop recording", e)
|
|
191
|
+
} finally {
|
|
192
|
+
audioRecord = null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
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
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (audioDataBuffer.size() > 0) {
|
|
217
|
+
emitAudioData() // Emit any remaining data
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
private fun clearAudioStorage() {
|
|
223
|
+
// Clear all files in the app's internal storage
|
|
224
|
+
val filesDir = appContext.reactContext?.filesDir
|
|
225
|
+
filesDir?.listFiles()?.forEach {
|
|
226
|
+
Log.d("AudioRecorderModule", "Deleting file: ${it.absolutePath}")
|
|
227
|
+
it.delete()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
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
|
|
239
|
+
val from = lastEmittedSize
|
|
240
|
+
val deltaSize = fileSize - lastEmittedSize
|
|
241
|
+
lastEmittedSize = fileSize
|
|
242
|
+
mainHandler.post {
|
|
243
|
+
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
|
+
)
|
|
254
|
+
} catch (e: Exception) {
|
|
255
|
+
Log.e("AudioRecorderModule", "Failed to send event", e)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
audioDataBuffer.reset() // Clear the buffer after emitting
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun encodeAudioData(rawData: ByteArray): String {
|
|
262
|
+
return Base64.encodeToString(rawData, Base64.DEFAULT)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private fun saveAudioToFile(rawData: ByteArray): Boolean {
|
|
266
|
+
return try {
|
|
267
|
+
// Open a FileOutputStream in append mode.
|
|
268
|
+
FileOutputStream(audioFile, true).use { output ->
|
|
269
|
+
// Write rawData to the file.
|
|
270
|
+
output.write(rawData)
|
|
271
|
+
}
|
|
272
|
+
true
|
|
273
|
+
} catch (e: IOException) {
|
|
274
|
+
// Handle exceptions here
|
|
275
|
+
Log.e("AudioRecorderModule", "Could not write to file: ${audioFile?.absolutePath}", e)
|
|
276
|
+
false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun checkPermission(): Boolean {
|
|
281
|
+
val reactContext = appContext.reactContext ?: return false // If reactContext is null, permissions cannot be checked
|
|
282
|
+
return ContextCompat.checkSelfPermission(reactContext, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private fun pauseRecording(promise: Promise) {
|
|
286
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
287
|
+
audioRecord?.stop()
|
|
288
|
+
lastPauseTime = System.currentTimeMillis() // Record the time when the recording was paused
|
|
289
|
+
isPaused.set(true)
|
|
290
|
+
promise.resolve("Recording paused")
|
|
291
|
+
} else {
|
|
292
|
+
promise.reject("NOT_RECORDING_OR_ALREADY_PAUSED", "Recording is either not active or already paused", null)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@SuppressLint("MissingPermission")
|
|
297
|
+
private fun initializeRecorder(): Boolean {
|
|
298
|
+
Log.d("AudioRecorderModule", "Initializing recorder")
|
|
299
|
+
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
300
|
+
Log.d("AudioRecorderModule", "Buffer size: $bufferSizeInBytes")
|
|
301
|
+
|
|
302
|
+
if (bufferSizeInBytes == AudioRecord.ERROR_BAD_VALUE) {
|
|
303
|
+
Log.e("AudioRecorderModule", "Invalid buffer size")
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
val recorder = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes)
|
|
308
|
+
if (recorder.state != AudioRecord.STATE_INITIALIZED) {
|
|
309
|
+
Log.e("AudioRecorderModule", "AudioRecord initialization failed")
|
|
310
|
+
recorder.release() // Clean up resources if initialization fails
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.audioRecord = recorder // Properly assign the recorder to the class member
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AudioEventPayload {
|
|
2
|
+
encoded?: string;
|
|
3
|
+
buffer?: Blob;
|
|
4
|
+
fileUri: string;
|
|
5
|
+
from: number;
|
|
6
|
+
deltaSize: number;
|
|
7
|
+
totalSize: number;
|
|
8
|
+
streamUuid: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AudioStreamStatus {
|
|
11
|
+
isRecording: boolean;
|
|
12
|
+
isPaused: boolean;
|
|
13
|
+
duration: number;
|
|
14
|
+
size: number;
|
|
15
|
+
interval: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RecordingOptions {
|
|
18
|
+
sampleRate?: number;
|
|
19
|
+
channelConfig?: number;
|
|
20
|
+
audioFormat?: number;
|
|
21
|
+
interval?: number;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=ExpoAudioStream.types.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.ts"],"names":[],"mappings":";AAIA,wBAAsD"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
// It loads the native module object from the JSI or falls back to
|
|
3
|
+
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
|
4
|
+
export default requireNativeModule('ExpoAudioStream');
|
|
5
|
+
//# sourceMappingURL=ExpoAudioStreamModule.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EventEmitter } from "expo-modules-core";
|
|
2
|
+
import { RecordingOptions } from "./ExpoAudioStream.types";
|
|
3
|
+
declare class ExpoAudioStreamWeb extends EventEmitter {
|
|
4
|
+
mediaRecorder: MediaRecorder | null;
|
|
5
|
+
audioChunks: Blob[];
|
|
6
|
+
isRecording: boolean;
|
|
7
|
+
isPaused: boolean;
|
|
8
|
+
recordingStartTime: number;
|
|
9
|
+
pausedTime: number;
|
|
10
|
+
currentDuration: number;
|
|
11
|
+
currentSize: number;
|
|
12
|
+
currentInterval: number;
|
|
13
|
+
lastEmittedSize: number;
|
|
14
|
+
streamUuid: string | null;
|
|
15
|
+
constructor();
|
|
16
|
+
getMediaStream(): Promise<MediaStream>;
|
|
17
|
+
startRecording(options?: RecordingOptions): Promise<void>;
|
|
18
|
+
setupRecordingListeners(): void;
|
|
19
|
+
emitAudioEvent(data: Blob): void;
|
|
20
|
+
generateUUID(): string;
|
|
21
|
+
stopRecording(): Promise<number>;
|
|
22
|
+
pauseRecording(): Promise<void>;
|
|
23
|
+
status(): {
|
|
24
|
+
isRecording: boolean;
|
|
25
|
+
isPaused: boolean;
|
|
26
|
+
duration: number;
|
|
27
|
+
size: number;
|
|
28
|
+
interval: number;
|
|
29
|
+
};
|
|
30
|
+
listAudioFiles(): void;
|
|
31
|
+
clearAudioFiles(): void;
|
|
32
|
+
}
|
|
33
|
+
declare const _default: ExpoAudioStreamWeb;
|
|
34
|
+
export default _default;
|
|
35
|
+
//# sourceMappingURL=ExpoAudioStreamModule.web.d.ts.map
|
|
@@ -0,0 +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"}
|