nosnia-audio-recorder 0.2.0 β 0.3.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 +46 -2
- package/android/src/main/java/com/nosniaaudiorecorder/NosniaAudioPlayerModule.kt +238 -0
- package/android/src/main/java/com/nosniaaudiorecorder/NosniaAudioRecorderModule.kt +204 -50
- package/android/src/main/java/com/nosniaaudiorecorder/NosniaAudioRecorderPackage.kt +12 -4
- package/ios/NosniaAudioPlayer.h +7 -0
- package/ios/NosniaAudioPlayer.mm +263 -0
- package/ios/NosniaAudioRecorder.mm +261 -44
- package/lib/module/AudioPlayer.js +176 -0
- package/lib/module/AudioPlayer.js.map +1 -0
- package/lib/module/NativeNosniaAudioPlayer.js +5 -0
- package/lib/module/NativeNosniaAudioPlayer.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/AudioPlayer.d.ts +76 -0
- package/lib/typescript/src/AudioPlayer.d.ts.map +1 -0
- package/lib/typescript/src/NativeNosniaAudioPlayer.d.ts +24 -0
- package/lib/typescript/src/NativeNosniaAudioPlayer.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +19 -7
- package/src/AudioPlayer.tsx +201 -0
- package/src/NativeNosniaAudioPlayer.ts +26 -0
- package/src/index.tsx +9 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A compact, high-performance audio recorder library for React Native that records
|
|
|
9
9
|
- ποΈ **Configurable**: Adjust bitrate, sample rate, and channels
|
|
10
10
|
- βΈοΈ **Pause/Resume**: Full control over recording with pause and resume
|
|
11
11
|
- β±οΈ **Real-time Duration**: Get continuous recording duration updates via callback
|
|
12
|
+
- βΆοΈ **MP3 Playback**: Built-in audio player with play/pause/resume/stop/seek
|
|
12
13
|
- π **Permission Handling**: Built-in permission checking and requesting
|
|
13
14
|
- πΎ **File Management**: Automatic file naming and directory management
|
|
14
15
|
- π¦ **Compact**: Minimal dependencies, optimized for bundle size
|
|
@@ -49,10 +50,10 @@ const filePath = await NosniaAudioRecorder.stopRecording();
|
|
|
49
50
|
console.log('Recording saved to:', filePath);
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
### Full Example
|
|
53
|
+
### Full Example (Record + Playback)
|
|
53
54
|
|
|
54
55
|
```js
|
|
55
|
-
import { NosniaAudioRecorder } from 'nosnia-audio-recorder';
|
|
56
|
+
import { NosniaAudioRecorder, NosniaAudioPlayer } from 'nosnia-audio-recorder';
|
|
56
57
|
import { useState } from 'react';
|
|
57
58
|
|
|
58
59
|
// Check and request permission
|
|
@@ -87,6 +88,16 @@ removeListener();
|
|
|
87
88
|
|
|
88
89
|
// Or cancel (discard)
|
|
89
90
|
await NosniaAudioRecorder.cancelRecording();
|
|
91
|
+
|
|
92
|
+
// Play it back
|
|
93
|
+
await NosniaAudioPlayer.startPlaying({ filePath });
|
|
94
|
+
// Optionally listen to progress
|
|
95
|
+
const removePlayback = NosniaAudioPlayer.addPlaybackProgressListener(({ currentTime, duration }) => {
|
|
96
|
+
console.log(`Progress: ${Math.floor(currentTime / 1000)} / ${Math.floor(duration / 1000)}s`);
|
|
97
|
+
});
|
|
98
|
+
// Stop playback
|
|
99
|
+
await NosniaAudioPlayer.stopPlaying();
|
|
100
|
+
removePlayback();
|
|
90
101
|
```
|
|
91
102
|
|
|
92
103
|
## API Reference
|
|
@@ -104,6 +115,20 @@ await NosniaAudioRecorder.cancelRecording();
|
|
|
104
115
|
- `addRecordingProgressListener(callback: RecordingProgressCallback): () => void` - Add listener for duration updates (returns cleanup function)
|
|
105
116
|
- `removeRecordingProgressListener(): void` - Remove duration listener
|
|
106
117
|
|
|
118
|
+
### Player Methods
|
|
119
|
+
|
|
120
|
+
- `startPlaying(options: PlayOptions): Promise<void>` - Start playing an audio file
|
|
121
|
+
- `stopPlaying(): Promise<void>` - Stop playback and reset
|
|
122
|
+
- `pausePlaying(): Promise<void>` - Pause playback
|
|
123
|
+
- `resumePlaying(): Promise<void>` - Resume playback
|
|
124
|
+
- `seekToTime(time: number): Promise<void>` - Seek to time (seconds)
|
|
125
|
+
- `setVolume(volume: number): Promise<void>` - Set playback volume (0.0β1.0)
|
|
126
|
+
- `getPlayerStatus(): Promise<PlayerStatus>` - Get playback status
|
|
127
|
+
- `addPlaybackProgressListener(callback: PlaybackProgressCallback): () => void` - Listen to playback progress
|
|
128
|
+
- `addPlaybackCompleteListener(callback: () => void): () => void` - Listen for playback completion
|
|
129
|
+
- `removePlaybackProgressListener(): void` - Remove progress listener
|
|
130
|
+
- `removePlaybackCompleteListener(): void` - Remove completion listener
|
|
131
|
+
|
|
107
132
|
### Types
|
|
108
133
|
|
|
109
134
|
```ts
|
|
@@ -124,6 +149,25 @@ type RecordingProgressCallback = (data: {
|
|
|
124
149
|
duration: number; // In milliseconds
|
|
125
150
|
isRecording: boolean;
|
|
126
151
|
}) => void;
|
|
152
|
+
|
|
153
|
+
interface PlayOptions {
|
|
154
|
+
filePath: string;
|
|
155
|
+
volume?: number; // 0.0 - 1.0
|
|
156
|
+
loop?: boolean; // repeat playback
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface PlayerStatus {
|
|
160
|
+
isPlaying: boolean;
|
|
161
|
+
duration: number; // In milliseconds
|
|
162
|
+
currentTime: number; // In milliseconds
|
|
163
|
+
currentFilePath?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type PlaybackProgressCallback = (data: {
|
|
167
|
+
currentTime: number;
|
|
168
|
+
duration: number;
|
|
169
|
+
isPlaying: boolean;
|
|
170
|
+
}) => void;
|
|
127
171
|
```
|
|
128
172
|
|
|
129
173
|
## Platform Setup
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
package com.nosniaaudiorecorder
|
|
2
|
+
|
|
3
|
+
import android.media.MediaPlayer
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
11
|
+
import com.facebook.react.bridge.Arguments
|
|
12
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
13
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
14
|
+
import java.io.File
|
|
15
|
+
|
|
16
|
+
@ReactModule(name = NosniaAudioPlayerModule.NAME)
|
|
17
|
+
class NosniaAudioPlayerModule(reactContext: ReactApplicationContext) :
|
|
18
|
+
NativeNosniaAudioPlayerSpec(reactContext) {
|
|
19
|
+
|
|
20
|
+
private var mediaPlayer: MediaPlayer? = null
|
|
21
|
+
private var currentFilePath: String? = null
|
|
22
|
+
private var isPlaying = false
|
|
23
|
+
|
|
24
|
+
private val progressHandler = Handler(Looper.getMainLooper())
|
|
25
|
+
private val progressRunnable = object : Runnable {
|
|
26
|
+
override fun run() {
|
|
27
|
+
mediaPlayer?.let { player ->
|
|
28
|
+
if (isPlaying && player.isPlaying) {
|
|
29
|
+
val currentTime = player.currentPosition.toLong()
|
|
30
|
+
val duration = player.duration.toLong()
|
|
31
|
+
|
|
32
|
+
sendEvent("onPlaybackProgress", Arguments.createMap().apply {
|
|
33
|
+
putDouble("currentTime", currentTime.toDouble())
|
|
34
|
+
putDouble("duration", duration.toDouble())
|
|
35
|
+
putBoolean("isPlaying", true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
progressHandler.postDelayed(this, 100)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun sendEvent(eventName: String, params: WritableMap?) {
|
|
45
|
+
reactApplicationContext
|
|
46
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
47
|
+
.emit(eventName, params)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun startProgressUpdates() {
|
|
51
|
+
progressHandler.post(progressRunnable)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun stopProgressUpdates() {
|
|
55
|
+
progressHandler.removeCallbacks(progressRunnable)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun getName(): String {
|
|
59
|
+
return NAME
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun startPlaying(options: ReadableMap, promise: Promise) {
|
|
63
|
+
try {
|
|
64
|
+
val filePath = options.getString("filePath")
|
|
65
|
+
if (filePath == null) {
|
|
66
|
+
promise.reject("INVALID_PATH", "File path is required")
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
val volume = if (options.hasKey("volume")) {
|
|
71
|
+
options.getDouble("volume").toFloat()
|
|
72
|
+
} else {
|
|
73
|
+
1.0f
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
val loop = if (options.hasKey("loop")) {
|
|
77
|
+
options.getBoolean("loop")
|
|
78
|
+
} else {
|
|
79
|
+
false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if file exists
|
|
83
|
+
val file = File(filePath)
|
|
84
|
+
if (!file.exists()) {
|
|
85
|
+
promise.reject("FILE_NOT_FOUND", "Audio file not found: $filePath")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Release existing player if any
|
|
90
|
+
mediaPlayer?.release()
|
|
91
|
+
|
|
92
|
+
mediaPlayer = MediaPlayer().apply {
|
|
93
|
+
setDataSource(filePath)
|
|
94
|
+
setVolume(volume, volume)
|
|
95
|
+
isLooping = loop
|
|
96
|
+
|
|
97
|
+
setOnCompletionListener {
|
|
98
|
+
isPlaying = false
|
|
99
|
+
stopProgressUpdates()
|
|
100
|
+
sendEvent("onPlaybackComplete", Arguments.createMap())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setOnErrorListener { _, what, extra ->
|
|
104
|
+
isPlaying = false
|
|
105
|
+
stopProgressUpdates()
|
|
106
|
+
true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
prepare()
|
|
110
|
+
start()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isPlaying = true
|
|
114
|
+
currentFilePath = filePath
|
|
115
|
+
startProgressUpdates()
|
|
116
|
+
promise.resolve(null)
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
mediaPlayer?.release()
|
|
119
|
+
mediaPlayer = null
|
|
120
|
+
isPlaying = false
|
|
121
|
+
promise.reject("START_PLAYING_ERROR", e.message, e)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun stopPlaying(promise: Promise) {
|
|
126
|
+
try {
|
|
127
|
+
stopProgressUpdates()
|
|
128
|
+
|
|
129
|
+
mediaPlayer?.apply {
|
|
130
|
+
if (isPlaying) {
|
|
131
|
+
stop()
|
|
132
|
+
}
|
|
133
|
+
release()
|
|
134
|
+
}
|
|
135
|
+
mediaPlayer = null
|
|
136
|
+
isPlaying = false
|
|
137
|
+
currentFilePath = null
|
|
138
|
+
|
|
139
|
+
promise.resolve(null)
|
|
140
|
+
} catch (e: Exception) {
|
|
141
|
+
promise.reject("STOP_PLAYING_ERROR", e.message, e)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun pausePlaying(promise: Promise) {
|
|
146
|
+
try {
|
|
147
|
+
if (!isPlaying || mediaPlayer == null) {
|
|
148
|
+
promise.reject("NOT_PLAYING", "No playback in progress")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mediaPlayer?.pause()
|
|
153
|
+
isPlaying = false
|
|
154
|
+
promise.resolve(null)
|
|
155
|
+
} catch (e: Exception) {
|
|
156
|
+
promise.reject("PAUSE_ERROR", e.message, e)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
override fun resumePlaying(promise: Promise) {
|
|
161
|
+
try {
|
|
162
|
+
if (isPlaying || mediaPlayer == null) {
|
|
163
|
+
promise.reject("NOT_PAUSED", "Playback is not paused")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
mediaPlayer?.start()
|
|
168
|
+
isPlaying = true
|
|
169
|
+
promise.resolve(null)
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
promise.reject("RESUME_ERROR", e.message, e)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override fun seekToTime(time: Double, promise: Promise) {
|
|
176
|
+
try {
|
|
177
|
+
if (mediaPlayer == null) {
|
|
178
|
+
promise.reject("NO_PLAYER", "No audio player initialized")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
val timeMs = (time * 1000).toInt()
|
|
183
|
+
mediaPlayer?.seekTo(timeMs)
|
|
184
|
+
promise.resolve(null)
|
|
185
|
+
} catch (e: Exception) {
|
|
186
|
+
promise.reject("SEEK_ERROR", e.message, e)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override fun setVolume(volume: Double, promise: Promise) {
|
|
191
|
+
try {
|
|
192
|
+
if (mediaPlayer == null) {
|
|
193
|
+
promise.reject("NO_PLAYER", "No audio player initialized")
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
val volumeFloat = volume.toFloat()
|
|
198
|
+
mediaPlayer?.setVolume(volumeFloat, volumeFloat)
|
|
199
|
+
promise.resolve(null)
|
|
200
|
+
} catch (e: Exception) {
|
|
201
|
+
promise.reject("SET_VOLUME_ERROR", e.message, e)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override fun getPlayerStatus(promise: Promise) {
|
|
206
|
+
try {
|
|
207
|
+
val status = WritableNativeMap().apply {
|
|
208
|
+
putBoolean("isPlaying", isPlaying)
|
|
209
|
+
|
|
210
|
+
if (mediaPlayer != null) {
|
|
211
|
+
putDouble("duration", mediaPlayer!!.duration.toDouble())
|
|
212
|
+
putDouble("currentTime", mediaPlayer!!.currentPosition.toDouble())
|
|
213
|
+
} else {
|
|
214
|
+
putDouble("duration", 0.0)
|
|
215
|
+
putDouble("currentTime", 0.0)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (currentFilePath != null) {
|
|
219
|
+
putString("currentFilePath", currentFilePath)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
promise.resolve(status)
|
|
223
|
+
} catch (e: Exception) {
|
|
224
|
+
promise.reject("STATUS_ERROR", e.message, e)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun onCatalystInstanceDestroy() {
|
|
229
|
+
super.onCatalystInstanceDestroy()
|
|
230
|
+
stopProgressUpdates()
|
|
231
|
+
mediaPlayer?.release()
|
|
232
|
+
mediaPlayer = null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
companion object {
|
|
236
|
+
const val NAME = "NosniaAudioPlayer"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -72,39 +72,109 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
|
|
|
72
72
|
|
|
73
73
|
override fun startRecording(options: ReadableMap, promise: Promise) {
|
|
74
74
|
try {
|
|
75
|
+
// Prevent concurrent recordings
|
|
75
76
|
if (isRecording) {
|
|
76
77
|
promise.reject("ALREADY_RECORDING", "Recording is already in progress")
|
|
77
78
|
return
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
val
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// Validate and get filename
|
|
82
|
+
val filename = options.getString("filename")?.takeIf { it.isNotBlank() } ?: generateFilename()
|
|
83
|
+
|
|
84
|
+
// Validate and sanitize audio parameters with bounds checking
|
|
85
|
+
val bitrate = options.getInt("bitrate").let {
|
|
86
|
+
when {
|
|
87
|
+
it <= 0 -> 128000 // Default
|
|
88
|
+
it > 320000 -> 320000 // Max reasonable bitrate
|
|
89
|
+
else -> it
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
val channels = options.getInt("channels").let {
|
|
94
|
+
when {
|
|
95
|
+
it <= 0 -> 1 // Default to mono
|
|
96
|
+
it > 2 -> 2 // Max 2 channels (stereo)
|
|
97
|
+
else -> it
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
val sampleRate = options.getInt("sampleRate").let {
|
|
102
|
+
when {
|
|
103
|
+
it <= 0 -> 44100 // Default
|
|
104
|
+
it < 8000 -> 8000 // Min reasonable sample rate
|
|
105
|
+
it > 48000 -> 48000 // Max reasonable sample rate
|
|
106
|
+
else -> it
|
|
107
|
+
}
|
|
108
|
+
}
|
|
84
109
|
|
|
110
|
+
// Get and validate recording directory
|
|
85
111
|
val recordingDir = getRecordingDirectory()
|
|
86
112
|
if (!recordingDir.exists()) {
|
|
87
|
-
recordingDir.mkdirs()
|
|
113
|
+
if (!recordingDir.mkdirs()) {
|
|
114
|
+
promise.reject("DIRECTORY_ERROR", "Failed to create recording directory")
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate directory is writable
|
|
120
|
+
if (!recordingDir.canWrite()) {
|
|
121
|
+
promise.reject("DIRECTORY_ERROR", "Recording directory is not writable")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create file path
|
|
126
|
+
val recordingFile = File(recordingDir, filename)
|
|
127
|
+
currentFilePath = recordingFile.absolutePath
|
|
128
|
+
|
|
129
|
+
if (currentFilePath == null || currentFilePath?.isEmpty() == true) {
|
|
130
|
+
promise.reject("PATH_ERROR", "Invalid file path")
|
|
131
|
+
return
|
|
88
132
|
}
|
|
89
133
|
|
|
90
|
-
|
|
134
|
+
// Clean up any existing file
|
|
135
|
+
try {
|
|
136
|
+
if (recordingFile.exists()) {
|
|
137
|
+
recordingFile.delete()
|
|
138
|
+
}
|
|
139
|
+
} catch (e: Exception) {
|
|
140
|
+
// Continue even if cleanup fails
|
|
141
|
+
}
|
|
91
142
|
|
|
143
|
+
// Create and configure media recorder
|
|
92
144
|
mediaRecorder = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
93
145
|
MediaRecorder(reactApplicationContext)
|
|
94
146
|
} else {
|
|
95
147
|
@Suppress("DEPRECATION")
|
|
96
148
|
MediaRecorder()
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (mediaRecorder == null) {
|
|
152
|
+
promise.reject("INIT_RECORDER_ERROR", "Failed to create MediaRecorder instance")
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
mediaRecorder?.apply {
|
|
158
|
+
setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
159
|
+
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
|
160
|
+
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
|
161
|
+
setAudioEncodingBitRate(bitrate)
|
|
162
|
+
setAudioChannels(channels)
|
|
163
|
+
setAudioSamplingRate(sampleRate)
|
|
164
|
+
setOutputFile(currentFilePath)
|
|
165
|
+
|
|
166
|
+
// Prepare recorder
|
|
167
|
+
prepare()
|
|
168
|
+
|
|
169
|
+
// Start recording
|
|
170
|
+
start()
|
|
171
|
+
}
|
|
172
|
+
} catch (e: Exception) {
|
|
173
|
+
mediaRecorder?.release()
|
|
174
|
+
mediaRecorder = null
|
|
175
|
+
currentFilePath = null
|
|
176
|
+
promise.reject("START_RECORDING_ERROR", e.message ?: "Failed to start recording", e)
|
|
177
|
+
return
|
|
108
178
|
}
|
|
109
179
|
|
|
110
180
|
isRecording = true
|
|
@@ -115,75 +185,122 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
|
|
|
115
185
|
mediaRecorder?.release()
|
|
116
186
|
mediaRecorder = null
|
|
117
187
|
isRecording = false
|
|
118
|
-
promise.reject("START_RECORDING_ERROR", e.message, e)
|
|
188
|
+
promise.reject("START_RECORDING_ERROR", e.message ?: "Unknown error", e)
|
|
119
189
|
}
|
|
120
190
|
}
|
|
121
191
|
|
|
122
192
|
override fun stopRecording(promise: Promise) {
|
|
123
193
|
try {
|
|
124
|
-
if (!isRecording
|
|
194
|
+
if (!isRecording || mediaRecorder == null) {
|
|
125
195
|
promise.reject("NOT_RECORDING", "No recording in progress")
|
|
126
196
|
return
|
|
127
197
|
}
|
|
128
198
|
|
|
129
199
|
stopProgressUpdates()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
mediaRecorder?.apply {
|
|
203
|
+
if (isRecording) {
|
|
204
|
+
stop()
|
|
205
|
+
}
|
|
206
|
+
release()
|
|
207
|
+
}
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
// Try to release even if stop fails
|
|
210
|
+
try {
|
|
211
|
+
mediaRecorder?.release()
|
|
212
|
+
} catch (releaseError: Exception) {
|
|
213
|
+
// Ignore
|
|
214
|
+
}
|
|
133
215
|
}
|
|
216
|
+
|
|
134
217
|
mediaRecorder = null
|
|
135
218
|
isRecording = false
|
|
136
219
|
isPaused = false
|
|
137
220
|
|
|
221
|
+
// Validate file exists before returning
|
|
138
222
|
val filePath = currentFilePath ?: ""
|
|
223
|
+
if (filePath.isNotEmpty()) {
|
|
224
|
+
try {
|
|
225
|
+
if (!File(filePath).exists()) {
|
|
226
|
+
promise.reject("FILE_NOT_FOUND", "Recording file was not created")
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
} catch (e: Exception) {
|
|
230
|
+
// Continue anyway, file might exist
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
139
234
|
currentFilePath = null
|
|
140
235
|
promise.resolve(filePath)
|
|
141
236
|
} catch (e: Exception) {
|
|
142
237
|
stopProgressUpdates()
|
|
143
|
-
|
|
238
|
+
try {
|
|
239
|
+
mediaRecorder?.release()
|
|
240
|
+
} catch (releaseError: Exception) {
|
|
241
|
+
// Ignore
|
|
242
|
+
}
|
|
144
243
|
mediaRecorder = null
|
|
145
244
|
isRecording = false
|
|
146
|
-
promise.reject("STOP_RECORDING_ERROR", e.message, e)
|
|
245
|
+
promise.reject("STOP_RECORDING_ERROR", e.message ?: "Failed to stop recording", e)
|
|
147
246
|
}
|
|
148
247
|
}
|
|
149
248
|
|
|
150
249
|
override fun pauseRecording(promise: Promise) {
|
|
151
250
|
try {
|
|
152
|
-
if (!isRecording) {
|
|
251
|
+
if (!isRecording || mediaRecorder == null) {
|
|
153
252
|
promise.reject("NOT_RECORDING", "No recording in progress")
|
|
154
253
|
return
|
|
155
254
|
}
|
|
156
255
|
|
|
256
|
+
if (isPaused) {
|
|
257
|
+
promise.reject("ALREADY_PAUSED", "Recording is already paused")
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
157
261
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
262
|
+
try {
|
|
263
|
+
mediaRecorder?.pause()
|
|
264
|
+
isPaused = true
|
|
265
|
+
pauseStartTime = System.currentTimeMillis()
|
|
266
|
+
promise.resolve(null)
|
|
267
|
+
} catch (e: Exception) {
|
|
268
|
+
promise.reject("PAUSE_ERROR", e.message ?: "Failed to pause recording", e)
|
|
269
|
+
}
|
|
162
270
|
} else {
|
|
163
|
-
promise.reject("
|
|
271
|
+
promise.reject("PAUSE_NOT_SUPPORTED", "Pause is not supported on this Android version")
|
|
164
272
|
}
|
|
165
273
|
} catch (e: Exception) {
|
|
166
|
-
promise.reject("PAUSE_ERROR", e.message, e)
|
|
274
|
+
promise.reject("PAUSE_ERROR", e.message ?: "Unknown error during pause", e)
|
|
167
275
|
}
|
|
168
276
|
}
|
|
169
277
|
|
|
170
278
|
override fun resumeRecording(promise: Promise) {
|
|
171
279
|
try {
|
|
172
|
-
if (!isRecording ||
|
|
280
|
+
if (!isRecording || mediaRecorder == null) {
|
|
281
|
+
promise.reject("NOT_RECORDING", "No recording in progress")
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!isPaused) {
|
|
173
286
|
promise.reject("NOT_PAUSED", "Recording is not paused")
|
|
174
287
|
return
|
|
175
288
|
}
|
|
176
289
|
|
|
177
290
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
291
|
+
try {
|
|
292
|
+
mediaRecorder?.resume()
|
|
293
|
+
isPaused = false
|
|
294
|
+
pausedDuration += System.currentTimeMillis() - pauseStartTime
|
|
295
|
+
promise.resolve(null)
|
|
296
|
+
} catch (e: Exception) {
|
|
297
|
+
promise.reject("RESUME_ERROR", e.message ?: "Failed to resume recording", e)
|
|
298
|
+
}
|
|
182
299
|
} else {
|
|
183
|
-
promise.reject("
|
|
300
|
+
promise.reject("RESUME_NOT_SUPPORTED", "Resume is not supported on this Android version")
|
|
184
301
|
}
|
|
185
302
|
} catch (e: Exception) {
|
|
186
|
-
promise.reject("RESUME_ERROR", e.message, e)
|
|
303
|
+
promise.reject("RESUME_ERROR", e.message ?: "Unknown error during resume", e)
|
|
187
304
|
}
|
|
188
305
|
}
|
|
189
306
|
|
|
@@ -191,46 +308,83 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
|
|
|
191
308
|
try {
|
|
192
309
|
stopProgressUpdates()
|
|
193
310
|
|
|
194
|
-
|
|
311
|
+
// Safely stop recorder
|
|
312
|
+
try {
|
|
313
|
+
mediaRecorder?.apply {
|
|
314
|
+
if (isRecording || isPaused) {
|
|
315
|
+
stop()
|
|
316
|
+
}
|
|
317
|
+
release()
|
|
318
|
+
}
|
|
319
|
+
} catch (e: Exception) {
|
|
320
|
+
// Try to release even if stop fails
|
|
195
321
|
try {
|
|
196
|
-
|
|
197
|
-
} catch (
|
|
198
|
-
//
|
|
322
|
+
mediaRecorder?.release()
|
|
323
|
+
} catch (releaseError: Exception) {
|
|
324
|
+
// Ignore
|
|
199
325
|
}
|
|
200
|
-
release()
|
|
201
326
|
}
|
|
327
|
+
|
|
202
328
|
mediaRecorder = null
|
|
203
329
|
|
|
330
|
+
// Safely delete recording file
|
|
204
331
|
currentFilePath?.let { filePath ->
|
|
205
|
-
|
|
332
|
+
try {
|
|
333
|
+
val file = File(filePath)
|
|
334
|
+
if (file.exists()) {
|
|
335
|
+
file.delete()
|
|
336
|
+
}
|
|
337
|
+
} catch (e: Exception) {
|
|
338
|
+
// Log but don't fail - file cleanup is not critical
|
|
339
|
+
}
|
|
206
340
|
}
|
|
341
|
+
|
|
207
342
|
currentFilePath = null
|
|
208
343
|
isRecording = false
|
|
209
344
|
isPaused = false
|
|
345
|
+
pausedDuration = 0
|
|
346
|
+
pauseStartTime = 0
|
|
210
347
|
|
|
211
348
|
promise.resolve(null)
|
|
212
349
|
} catch (e: Exception) {
|
|
213
|
-
|
|
350
|
+
mediaRecorder = null
|
|
351
|
+
isRecording = false
|
|
352
|
+
promise.reject("CANCEL_ERROR", e.message ?: "Failed to cancel recording", e)
|
|
214
353
|
}
|
|
215
354
|
}
|
|
216
355
|
|
|
217
356
|
override fun getRecorderStatus(promise: Promise) {
|
|
218
357
|
try {
|
|
219
358
|
val currentTime = System.currentTimeMillis()
|
|
220
|
-
|
|
359
|
+
|
|
360
|
+
// Safely calculate duration
|
|
361
|
+
val duration = if (isRecording && mediaRecorder != null) {
|
|
221
362
|
if (isPaused) {
|
|
222
|
-
|
|
363
|
+
// Paused: duration = pause time - start time - previous paused duration
|
|
364
|
+
val pausedDur = pauseStartTime - startTime - pausedDuration
|
|
365
|
+
if (pausedDur < 0) 0L else pausedDur
|
|
223
366
|
} else {
|
|
224
|
-
|
|
367
|
+
// Recording: duration = current time - start time - previous paused duration
|
|
368
|
+
val recordingDur = currentTime - startTime - pausedDuration
|
|
369
|
+
if (recordingDur < 0) 0L else recordingDur
|
|
225
370
|
}
|
|
226
371
|
} else {
|
|
227
|
-
|
|
372
|
+
0L
|
|
228
373
|
}
|
|
229
374
|
|
|
230
375
|
val status = WritableNativeMap().apply {
|
|
231
376
|
putBoolean("isRecording", isRecording)
|
|
232
377
|
putDouble("duration", duration.toDouble())
|
|
233
|
-
if (currentFilePath
|
|
378
|
+
if (!currentFilePath.isNullOrEmpty()) {
|
|
379
|
+
putString("currentFilePath", currentFilePath)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
promise.resolve(status)
|
|
384
|
+
} catch (e: Exception) {
|
|
385
|
+
promise.reject("STATUS_ERROR", e.message ?: "Failed to get recorder status", e)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
234
388
|
putString("currentFilePath", currentFilePath)
|
|
235
389
|
}
|
|
236
390
|
}
|