nosnia-audio-recorder 0.1.1 β†’ 0.3.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/README.md CHANGED
@@ -8,6 +8,8 @@ A compact, high-performance audio recorder library for React Native that records
8
8
  - πŸ“± **Cross-Platform**: Works seamlessly on iOS and Android
9
9
  - 🎚️ **Configurable**: Adjust bitrate, sample rate, and channels
10
10
  - ⏸️ **Pause/Resume**: Full control over recording with pause and resume
11
+ - ⏱️ **Real-time Duration**: Get continuous recording duration updates via callback
12
+ - ▢️ **MP3 Playback**: Built-in audio player with play/pause/resume/stop/seek
11
13
  - πŸ”’ **Permission Handling**: Built-in permission checking and requesting
12
14
  - πŸ’Ύ **File Management**: Automatic file naming and directory management
13
15
  - πŸ“¦ **Compact**: Minimal dependencies, optimized for bundle size
@@ -48,10 +50,11 @@ const filePath = await NosniaAudioRecorder.stopRecording();
48
50
  console.log('Recording saved to:', filePath);
49
51
  ```
50
52
 
51
- ### Full Example
53
+ ### Full Example (Record + Playback)
52
54
 
53
55
  ```js
54
- import { NosniaAudioRecorder } from 'nosnia-audio-recorder';
56
+ import { NosniaAudioRecorder, NosniaAudioPlayer } from 'nosnia-audio-recorder';
57
+ import { useState } from 'react';
55
58
 
56
59
  // Check and request permission
57
60
  const hasPermission = await NosniaAudioRecorder.checkPermission();
@@ -59,9 +62,16 @@ if (!hasPermission) {
59
62
  await NosniaAudioRecorder.requestPermission();
60
63
  }
61
64
 
62
- // Start recording
65
+ // Start recording with real-time duration updates
63
66
  await NosniaAudioRecorder.startRecording();
64
67
 
68
+ // Add listener for recording duration updates (every 100ms)
69
+ const removeListener = NosniaAudioRecorder.addRecordingProgressListener(
70
+ ({ duration, isRecording }) => {
71
+ console.log(`Recording ${Math.floor(duration / 1000)}s`);
72
+ }
73
+ );
74
+
65
75
  // Pause if needed
66
76
  await NosniaAudioRecorder.pauseRecording();
67
77
  await NosniaAudioRecorder.resumeRecording();
@@ -73,8 +83,21 @@ console.log(`Recording: ${status.isRecording}, Duration: ${status.duration}ms`);
73
83
  // Stop and save
74
84
  const filePath = await NosniaAudioRecorder.stopRecording();
75
85
 
86
+ // Remove listener when done
87
+ removeListener();
88
+
76
89
  // Or cancel (discard)
77
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();
78
101
  ```
79
102
 
80
103
  ## API Reference
@@ -89,6 +112,22 @@ await NosniaAudioRecorder.cancelRecording();
89
112
  - `resumeRecording(): Promise<void>` - Resume recording
90
113
  - `cancelRecording(): Promise<void>` - Cancel and discard
91
114
  - `getStatus(): Promise<RecorderStatus>` - Get recorder status
115
+ - `addRecordingProgressListener(callback: RecordingProgressCallback): () => void` - Add listener for duration updates (returns cleanup function)
116
+ - `removeRecordingProgressListener(): void` - Remove duration listener
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
92
131
 
93
132
  ### Types
94
133
 
@@ -105,6 +144,30 @@ interface RecorderStatus {
105
144
  duration: number; // In milliseconds
106
145
  currentFilePath?: string;
107
146
  }
147
+
148
+ type RecordingProgressCallback = (data: {
149
+ duration: number; // In milliseconds
150
+ isRecording: boolean;
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;
108
171
  ```
109
172
 
110
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
+ }
@@ -5,12 +5,16 @@ import android.content.Context
5
5
  import android.media.MediaRecorder
6
6
  import android.os.Build
7
7
  import android.os.Environment
8
+ import android.os.Handler
9
+ import android.os.Looper
8
10
  import androidx.core.content.ContextCompat
9
11
  import com.facebook.react.bridge.Promise
10
12
  import com.facebook.react.bridge.ReactApplicationContext
11
13
  import com.facebook.react.bridge.ReadableMap
12
14
  import com.facebook.react.bridge.WritableMap
13
15
  import com.facebook.react.bridge.WritableNativeMap
16
+ import com.facebook.react.bridge.Arguments
17
+ import com.facebook.react.modules.core.DeviceEventManagerModule
14
18
  import com.facebook.react.module.annotations.ReactModule
15
19
  import java.io.File
16
20
  import java.text.SimpleDateFormat
@@ -25,6 +29,42 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
25
29
  private var currentFilePath: String? = null
26
30
  private var isRecording = false
27
31
  private var isPaused = false
32
+ private var startTime: Long = 0
33
+ private var pausedDuration: Long = 0
34
+ private var pauseStartTime: Long = 0
35
+
36
+ private val progressHandler = Handler(Looper.getMainLooper())
37
+ private val progressRunnable = object : Runnable {
38
+ override fun run() {
39
+ if (isRecording && !isPaused) {
40
+ val currentTime = System.currentTimeMillis()
41
+ val duration = currentTime - startTime - pausedDuration
42
+
43
+ sendEvent("onRecordingProgress", Arguments.createMap().apply {
44
+ putDouble("duration", duration.toDouble())
45
+ putBoolean("isRecording", true)
46
+ })
47
+
48
+ progressHandler.postDelayed(this, 100) // Update every 100ms
49
+ }
50
+ }
51
+ }
52
+
53
+ private fun sendEvent(eventName: String, params: WritableMap?) {
54
+ reactApplicationContext
55
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
56
+ .emit(eventName, params)
57
+ }
58
+
59
+ private fun startProgressUpdates() {
60
+ startTime = System.currentTimeMillis()
61
+ pausedDuration = 0
62
+ progressHandler.post(progressRunnable)
63
+ }
64
+
65
+ private fun stopProgressUpdates() {
66
+ progressHandler.removeCallbacks(progressRunnable)
67
+ }
28
68
 
29
69
  override fun getName(): String {
30
70
  return NAME
@@ -69,6 +109,7 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
69
109
 
70
110
  isRecording = true
71
111
  isPaused = false
112
+ startProgressUpdates()
72
113
  promise.resolve(null)
73
114
  } catch (e: Exception) {
74
115
  mediaRecorder?.release()
@@ -85,6 +126,7 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
85
126
  return
86
127
  }
87
128
 
129
+ stopProgressUpdates()
88
130
  mediaRecorder?.apply {
89
131
  stop()
90
132
  release()
@@ -97,6 +139,7 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
97
139
  currentFilePath = null
98
140
  promise.resolve(filePath)
99
141
  } catch (e: Exception) {
142
+ stopProgressUpdates()
100
143
  mediaRecorder?.release()
101
144
  mediaRecorder = null
102
145
  isRecording = false
@@ -114,6 +157,7 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
114
157
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
115
158
  mediaRecorder?.pause()
116
159
  isPaused = true
160
+ pauseStartTime = System.currentTimeMillis()
117
161
  promise.resolve(null)
118
162
  } else {
119
163
  promise.reject("NOT_SUPPORTED", "Pause is not supported on this API level")
@@ -133,6 +177,7 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
133
177
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
134
178
  mediaRecorder?.resume()
135
179
  isPaused = false
180
+ pausedDuration += System.currentTimeMillis() - pauseStartTime
136
181
  promise.resolve(null)
137
182
  } else {
138
183
  promise.reject("NOT_SUPPORTED", "Resume is not supported on this API level")
@@ -144,6 +189,8 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
144
189
 
145
190
  override fun cancelRecording(promise: Promise) {
146
191
  try {
192
+ stopProgressUpdates()
193
+
147
194
  mediaRecorder?.apply {
148
195
  try {
149
196
  stop()
@@ -169,9 +216,20 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
169
216
 
170
217
  override fun getRecorderStatus(promise: Promise) {
171
218
  try {
219
+ val currentTime = System.currentTimeMillis()
220
+ val duration = if (isRecording) {
221
+ if (isPaused) {
222
+ pauseStartTime - startTime - pausedDuration
223
+ } else {
224
+ currentTime - startTime - pausedDuration
225
+ }
226
+ } else {
227
+ 0
228
+ }
229
+
172
230
  val status = WritableNativeMap().apply {
173
231
  putBoolean("isRecording", isRecording)
174
- putDouble("duration", mediaRecorder?.currentPosition?.toDouble() ?: 0.0)
232
+ putDouble("duration", duration.toDouble())
175
233
  if (currentFilePath != null) {
176
234
  putString("currentFilePath", currentFilePath)
177
235
  }
@@ -9,10 +9,10 @@ import java.util.HashMap
9
9
 
10
10
  class NosniaAudioRecorderPackage : BaseReactPackage() {
11
11
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
- return if (name == NosniaAudioRecorderModule.NAME) {
13
- NosniaAudioRecorderModule(reactContext)
14
- } else {
15
- null
12
+ return when (name) {
13
+ NosniaAudioRecorderModule.NAME -> NosniaAudioRecorderModule(reactContext)
14
+ NosniaAudioPlayerModule.NAME -> NosniaAudioPlayerModule(reactContext)
15
+ else -> null
16
16
  }
17
17
  }
18
18
 
@@ -27,6 +27,14 @@ class NosniaAudioRecorderPackage : BaseReactPackage() {
27
27
  false, // isCxxModule
28
28
  true // isTurboModule
29
29
  )
30
+ moduleInfos[NosniaAudioPlayerModule.NAME] = ReactModuleInfo(
31
+ NosniaAudioPlayerModule.NAME,
32
+ NosniaAudioPlayerModule.NAME,
33
+ false, // canOverrideExistingModule
34
+ false, // needsEagerInit
35
+ false, // isCxxModule
36
+ true // isTurboModule
37
+ )
30
38
  moduleInfos
31
39
  }
32
40
  }
@@ -0,0 +1,7 @@
1
+ #import <NosniaAudioPlayerSpec/NosniaAudioPlayerSpec.h>
2
+ #import <React/RCTBridgeModule.h>
3
+ #import <React/RCTEventEmitter.h>
4
+
5
+ @interface NosniaAudioPlayer : RCTEventEmitter <NativeNosniaAudioPlayerSpec>
6
+
7
+ @end