rns-recplay 2.0.4 → 3.0.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 +197 -125
- package/android/src/main/java/com/rnsrecplay/RecPlayModule.kt +228 -64
- package/index.d.ts +15 -26
- package/index.js +26 -6
- package/ios/RecPlayModule.m +1 -0
- package/ios/RecPlayModule.swift +239 -169
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,35 +1,20 @@
|
|
|
1
1
|
# 🎤 rns-recplay
|
|
2
2
|
|
|
3
|
-
A **high-performance React Native audio recording and playback
|
|
4
|
-
Designed for **voice notes,
|
|
3
|
+
A **high-performance React Native audio recording and audio playback** compatible with WebRTC.
|
|
4
|
+
Designed for **voice notes, audio workstations, and media apps**, offering real-time volume metering, smooth looping, precise seek, and automatic playback interruption handling.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## ✨ Features
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Powered by **ExoPlayer (Android)** and **AVPlayer (iOS)**
|
|
19
|
-
* Native-level performance
|
|
20
|
-
|
|
21
|
-
* 🔄 **Seamless Looping**
|
|
22
|
-
|
|
23
|
-
* Gapless looping handled natively
|
|
24
|
-
|
|
25
|
-
* 📊 **Progress Tracking**
|
|
26
|
-
|
|
27
|
-
* Position & duration updates every **500ms**
|
|
28
|
-
|
|
29
|
-
* 🛡️ **Smart Audio Control**
|
|
30
|
-
|
|
31
|
-
* Automatically stops playback when recording begins
|
|
32
|
-
* Prevents overlapping audio sessions
|
|
10
|
+
- 🎙️ **Audio Recording** — High-quality AAC / M4A, real-time timer, pause & resume
|
|
11
|
+
- 📊 **Real-time Volume Metering** — dB + normalized (0.0–1.0) every 100ms for UI visualizers
|
|
12
|
+
- 🔊 **Audio Playback** — Powered by **ExoPlayer (Android)** and **AVPlayer (iOS)**
|
|
13
|
+
- 🎯 **Precision Seek** — Zero-tolerance sample-accurate seeking
|
|
14
|
+
- 🔄 **Seamless Looping** — Gapless looping handled natively
|
|
15
|
+
- 📈 **Smooth Progress Tracking** — Position & duration updates every **50ms**
|
|
16
|
+
- 🛡️ **Smart Audio Control** — Audio focus, interruption handling, headphone unplug detection
|
|
17
|
+
- 🎙️ **WebRTC Compatible** — Detects active calls and adapts audio routing automatically
|
|
33
18
|
|
|
34
19
|
---
|
|
35
20
|
|
|
@@ -39,8 +24,6 @@ Designed for **voice notes, chats, and media apps**, offering smooth looping, pr
|
|
|
39
24
|
npm install rns-recplay
|
|
40
25
|
```
|
|
41
26
|
|
|
42
|
-
or (Expo):
|
|
43
|
-
|
|
44
27
|
```bash
|
|
45
28
|
npx expo install rns-recplay
|
|
46
29
|
```
|
|
@@ -49,185 +32,274 @@ npx expo install rns-recplay
|
|
|
49
32
|
|
|
50
33
|
## ⚙️ Expo Configuration
|
|
51
34
|
|
|
52
|
-
Add the plugin to your
|
|
35
|
+
Add the plugin to your `app.json` or `app.config.js`:
|
|
53
36
|
|
|
54
37
|
```json
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
38
|
+
{
|
|
39
|
+
"plugins": [
|
|
40
|
+
[
|
|
41
|
+
"rns-recplay",
|
|
42
|
+
{
|
|
43
|
+
"microphonePermission": "Allow microphone access for voice recording."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
]
|
|
47
|
+
}
|
|
61
48
|
```
|
|
62
49
|
|
|
63
50
|
---
|
|
64
51
|
|
|
65
52
|
## 🚀 Usage
|
|
66
53
|
|
|
67
|
-
### 🎙️ Recording
|
|
54
|
+
### 🎙️ Start & Stop Recording
|
|
68
55
|
|
|
69
56
|
```js
|
|
70
57
|
import Recplay from 'rns-recplay';
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
59
|
+
// Start recording
|
|
60
|
+
const fileName = await Recplay.startRecording({
|
|
61
|
+
fileName: "my_voice_note", // optional, defaults to rec_<timestamp>
|
|
62
|
+
shouldStopPlayback: true, // stop any playing audio first
|
|
63
|
+
duck: true, // lower other audio while recording
|
|
64
|
+
mixWithOthers: true, // mix with other audio sessions
|
|
65
|
+
useBT: false, // false = force built-in mic (recommended)
|
|
66
|
+
onSecondsUpdate: (seconds) => {
|
|
67
|
+
console.log(`Recorded: ${seconds}s`);
|
|
68
|
+
},
|
|
69
|
+
onVolumeUpdate: (db, normalized) => {
|
|
70
|
+
// db: raw decibel value (~-60 to 0)
|
|
71
|
+
// normalized: 0.0 to 1.0 — use this to drive a waveform visualizer
|
|
72
|
+
console.log(`Volume: ${normalized}`);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log("Recording started, file name:", fileName);
|
|
87
77
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
// Stop recording
|
|
79
|
+
const result = await Recplay.stopRecording();
|
|
80
|
+
console.log("URI:", result.uri); // file:///...path/to/file.m4a
|
|
81
|
+
console.log("Duration:", result.duration); // seconds, e.g. 5.4
|
|
92
82
|
```
|
|
93
83
|
|
|
94
84
|
---
|
|
95
85
|
|
|
96
|
-
###
|
|
86
|
+
### ⏸️ Pause & Resume Recording
|
|
97
87
|
|
|
98
88
|
```js
|
|
99
|
-
|
|
89
|
+
await Recplay.pauseRecording();
|
|
90
|
+
await Recplay.resumeRecording();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
100
94
|
|
|
95
|
+
### 🔊 Playback
|
|
96
|
+
|
|
97
|
+
```js
|
|
101
98
|
Recplay.playAudio({
|
|
102
99
|
uri: "file:///path/to/audio.m4a",
|
|
103
100
|
shouldStopPrevious: true,
|
|
104
|
-
loop:
|
|
101
|
+
loop: false,
|
|
105
102
|
mixWithOthers: true,
|
|
106
|
-
duck:
|
|
103
|
+
duck: true,
|
|
107
104
|
callbacks: {
|
|
108
|
-
onStatus: (status) =>
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
onStatus: (status) => {
|
|
106
|
+
// 'BUFFERING' | 'PLAYING' | 'PAUSED' | 'ENDED' | 'ERROR'
|
|
107
|
+
console.log("Status:", status);
|
|
108
|
+
if (status === 'PLAYING') {
|
|
109
|
+
// safe to seek or update UI
|
|
110
|
+
}
|
|
111
|
+
if (status === 'ENDED') {
|
|
112
|
+
// reset playhead
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
onProgress: (currentPosition, duration) => {
|
|
116
|
+
// fires every ~50ms while playing
|
|
117
|
+
console.log(`${currentPosition} / ${duration}`);
|
|
118
|
+
},
|
|
119
|
+
onPlaybackFinished: () => {
|
|
120
|
+
console.log("Playback finished");
|
|
121
|
+
},
|
|
111
122
|
}
|
|
112
123
|
});
|
|
113
124
|
```
|
|
114
125
|
|
|
115
126
|
---
|
|
116
127
|
|
|
117
|
-
|
|
128
|
+
### 🎯 Seek, Toggle & Stop
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
```js
|
|
131
|
+
// Seek to a specific time (sample-accurate)
|
|
132
|
+
Recplay.seekTo({ seconds: 30.5 });
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| `checkPermission()` | Checks the current microphone permission status. |
|
|
124
|
-
| `requestPermission()` | Triggers the system permission dialog. |
|
|
134
|
+
// Toggle between play and pause
|
|
135
|
+
Recplay.togglePlayback();
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
// Stop playback and release resources
|
|
138
|
+
await Recplay.stopPlayback();
|
|
139
|
+
```
|
|
127
140
|
|
|
128
|
-
|
|
141
|
+
---
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
### 🔐 Permissions
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// Check current status
|
|
147
|
+
const status = await Recplay.checkPermission();
|
|
148
|
+
// Returns: 'granted' | 'denied' | 'blocked' | 'unavailable'
|
|
149
|
+
|
|
150
|
+
// Request permission
|
|
151
|
+
const result = await Recplay.requestPermission();
|
|
152
|
+
// Returns: 'granted' | 'denied'
|
|
153
|
+
```
|
|
134
154
|
|
|
135
155
|
---
|
|
136
156
|
|
|
137
|
-
|
|
157
|
+
## 📚 API Reference
|
|
138
158
|
|
|
139
|
-
|
|
159
|
+
### `startRecording(options?)`
|
|
140
160
|
|
|
141
|
-
| Parameter | Type | Default | Description
|
|
142
|
-
|
|
143
|
-
| `fileName` | `string` | `null` |
|
|
144
|
-
| `shouldStopPlayback` | `boolean` | `true` |
|
|
145
|
-
| `duck` | `boolean` | `true` |
|
|
146
|
-
| `mixWithOthers` | `boolean` | `true` |
|
|
147
|
-
| `
|
|
161
|
+
| Parameter | Type | Default | Description |
|
|
162
|
+
|----------------------|------------|---------|---------------------------------------------------------------------|
|
|
163
|
+
| `fileName` | `string` | `null` | Output file name (without extension). Defaults to `rec_<timestamp>` |
|
|
164
|
+
| `shouldStopPlayback` | `boolean` | `true` | Stop any active playback before recording |
|
|
165
|
+
| `duck` | `boolean` | `true` | Lower volume of other audio while recording |
|
|
166
|
+
| `mixWithOthers` | `boolean` | `true` | Allow mixing with other active audio sessions |
|
|
167
|
+
| `useBT` | `boolean` | `false` | Use Bluetooth mic. `false` forces the built-in microphone |
|
|
168
|
+
| `onSecondsUpdate` | `function` | `null` | Called once per second with `(seconds: number)` |
|
|
169
|
+
| `onVolumeUpdate` | `function` | `null` | Called every 100ms with `(db: number, normalized: number)` |
|
|
148
170
|
|
|
149
|
-
**Returns:** `Promise<string>`
|
|
171
|
+
**Returns:** `Promise<string>` — the file name (without extension)
|
|
150
172
|
|
|
151
173
|
---
|
|
152
174
|
|
|
153
|
-
|
|
175
|
+
### `stopRecording()`
|
|
154
176
|
|
|
155
|
-
Stops recording and
|
|
177
|
+
Stops the active recording, releases the microphone, and returns the saved file info.
|
|
156
178
|
|
|
157
|
-
**Returns:** `Promise<string
|
|
179
|
+
**Returns:** `Promise<{ uri: string, duration: number }>`
|
|
180
|
+
|
|
181
|
+
| Field | Type | Description |
|
|
182
|
+
|------------|----------|-----------------------------------|
|
|
183
|
+
| `uri` | `string` | Full file URI, e.g. `file:///...` |
|
|
184
|
+
| `duration` | `number` | Duration in seconds, e.g. `5.4` |
|
|
158
185
|
|
|
159
186
|
---
|
|
160
187
|
|
|
161
|
-
|
|
188
|
+
### `pauseRecording()`
|
|
162
189
|
|
|
163
|
-
Pauses the active recording session.
|
|
190
|
+
Pauses the active recording session. Elapsed time is preserved accurately across pauses.
|
|
164
191
|
|
|
165
|
-
|
|
192
|
+
**Returns:** `Promise<boolean>`
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### `resumeRecording()`
|
|
166
197
|
|
|
167
198
|
Resumes a paused recording session.
|
|
168
199
|
|
|
200
|
+
**Returns:** `Promise<boolean>`
|
|
201
|
+
|
|
169
202
|
---
|
|
170
203
|
|
|
171
|
-
###
|
|
204
|
+
### `playAudio(options)`
|
|
205
|
+
|
|
206
|
+
| Parameter | Type | Default | Description |
|
|
207
|
+
|----------------------|-----------|---------|-------------------------------------------|
|
|
208
|
+
| `uri` | `string` | — | Audio file URI or HTTP URL |
|
|
209
|
+
| `shouldStopPrevious` | `boolean` | `false` | Stop and release any previous playback |
|
|
210
|
+
| `loop` | `boolean` | `false` | Loop playback natively (gapless) |
|
|
211
|
+
| `mixWithOthers` | `boolean` | `true` | Mix with other active audio sessions |
|
|
212
|
+
| `duck` | `boolean` | `false` | Lower volume of other audio while playing |
|
|
213
|
+
| `callbacks` | `object` | `{}` | Event callbacks (see below) |
|
|
172
214
|
|
|
173
|
-
####
|
|
215
|
+
#### Callbacks
|
|
174
216
|
|
|
175
|
-
|
|
|
176
|
-
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
| `callbacks` | `object` | `{}` | Playback event callbacks |
|
|
217
|
+
| Callback | Signature | Description |
|
|
218
|
+
|----------------------|-----------------------------------------------------------|-----------------------------------|
|
|
219
|
+
| `onStatus` | `(status: string) => void` | Player state changes (see below) |
|
|
220
|
+
| `onProgress` | `(currentPosition: number, duration: number) => void` | Fires every ~50ms while playing |
|
|
221
|
+
| `onPlaybackFinished` | `() => void` | Fires when non-looping audio ends |
|
|
222
|
+
|
|
223
|
+
---
|
|
183
224
|
|
|
184
|
-
|
|
225
|
+
### `stopPlayback()`
|
|
185
226
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
| `onProgress` | `(position, duration)` | Playback progress |
|
|
190
|
-
| `onPlaybackFinished` | `()` | Fired when playback ends |
|
|
227
|
+
Stops playback, removes all observers, and releases the player.
|
|
228
|
+
|
|
229
|
+
**Returns:** `Promise<boolean>`
|
|
191
230
|
|
|
192
231
|
---
|
|
193
232
|
|
|
194
|
-
|
|
233
|
+
### `togglePlayback()`
|
|
195
234
|
|
|
196
|
-
|
|
235
|
+
Toggles between play and pause on the current player instance.
|
|
197
236
|
|
|
198
|
-
|
|
237
|
+
---
|
|
199
238
|
|
|
200
|
-
|
|
239
|
+
### `seekTo({ seconds })`
|
|
201
240
|
|
|
202
|
-
|
|
241
|
+
Seeks to a precise position. Uses zero-tolerance seeking for sample accuracy.
|
|
203
242
|
|
|
204
|
-
| Parameter | Type | Description
|
|
205
|
-
|
|
206
|
-
| `seconds` | `number` |
|
|
243
|
+
| Parameter | Type | Description |
|
|
244
|
+
|-----------|----------|----------------------------|
|
|
245
|
+
| `seconds` | `number` | Target position in seconds |
|
|
207
246
|
|
|
208
247
|
---
|
|
209
248
|
|
|
210
|
-
|
|
249
|
+
### `checkPermission()`
|
|
250
|
+
|
|
251
|
+
**Returns:** `Promise<'granted' | 'denied' | 'blocked' | 'unavailable'>`
|
|
211
252
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
| Status | Meaning |
|
|
254
|
+
|---------------|--------------------------------------------------------------------------|
|
|
255
|
+
| `granted` | Microphone is available and permitted |
|
|
256
|
+
| `denied` | Not asked yet (iOS) or dismissed once (Android) — can still request |
|
|
257
|
+
| `blocked` | User selected "Don't Allow" / "Never ask again" — redirect to Settings |
|
|
258
|
+
| `unavailable` | Hardware missing or OS-restricted |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### `requestPermission()`
|
|
263
|
+
|
|
264
|
+
Triggers the native system permission dialog.
|
|
265
|
+
|
|
266
|
+
**Returns:** `Promise<'granted' | 'denied'>`
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 📌 Playback Status Values
|
|
271
|
+
|
|
272
|
+
| Status | When |
|
|
273
|
+
|-------------|-----------------------------------------------|
|
|
274
|
+
| `BUFFERING` | Loading / rebuffering (network or large file) |
|
|
275
|
+
| `PLAYING` | Audio is actively playing |
|
|
276
|
+
| `PAUSED` | Paused by user or audio focus loss |
|
|
277
|
+
| `ENDED` | Playback reached the end of the file |
|
|
278
|
+
| `ERROR` | Playback failed (bad URI, decode error, etc.) |
|
|
218
279
|
|
|
219
280
|
---
|
|
220
281
|
|
|
221
282
|
## 🛠️ Platform Support
|
|
222
283
|
|
|
223
284
|
| Platform | Supported |
|
|
224
|
-
|
|
225
|
-
| Android | ✅
|
|
226
|
-
| iOS | ✅
|
|
227
|
-
| Expo (Dev / EAS) | ✅
|
|
285
|
+
|------------------|-----------|
|
|
286
|
+
| Android | ✅ |
|
|
287
|
+
| iOS | ✅ |
|
|
288
|
+
| Expo (Dev / EAS) | ✅ |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 📝 Notes
|
|
293
|
+
|
|
294
|
+
- Recorded files are saved in the app's **cache directory** as `.m4a` (AAC, 44.1kHz, 128kbps, mono)
|
|
295
|
+
- `onVolumeUpdate` fires at 100ms intervals — use `normalized` (0.0–1.0) to drive waveform visualizer bars
|
|
296
|
+
- `onSecondsUpdate` fires once per second only on whole-second boundaries to avoid flooding JS
|
|
297
|
+
- `useBT: false` (default) forces the phone's **built-in microphone** even when Bluetooth headphones are connected — this prevents Lightning/Bluetooth accessories from switching to low-quality HFP mode
|
|
298
|
+
- `seekTo` is safe to call immediately after `onStatus: 'PLAYING'` fires
|
|
299
|
+
- Calling `stopPlayback` cleans up all native observers — no memory leaks
|
|
228
300
|
|
|
229
301
|
---
|
|
230
302
|
|
|
231
303
|
## 📄 License
|
|
232
304
|
|
|
233
|
-
MIT
|
|
305
|
+
MIT
|