rns-recplay
v3.0.0
Published
High-performance React Native module for audio recording and audio playback on Android and iOS.
Maintainers
Readme
🎤 rns-recplay
A high-performance React Native audio recording and audio playback compatible with WebRTC.
Designed for voice notes, audio workstations, and media apps, offering real-time volume metering, smooth looping, precise seek, and automatic playback interruption handling.
✨ Features
- 🎙️ Audio Recording — High-quality AAC / M4A, real-time timer, pause & resume
- 📊 Real-time Volume Metering — dB + normalized (0.0–1.0) every 100ms for UI visualizers
- 🔊 Audio Playback — Powered by ExoPlayer (Android) and AVPlayer (iOS)
- 🎯 Precision Seek — Zero-tolerance sample-accurate seeking
- 🔄 Seamless Looping — Gapless looping handled natively
- 📈 Smooth Progress Tracking — Position & duration updates every 50ms
- 🛡️ Smart Audio Control — Audio focus, interruption handling, headphone unplug detection
- 🎙️ WebRTC Compatible — Detects active calls and adapts audio routing automatically
📦 Installation
npm install rns-recplaynpx expo install rns-recplay⚙️ Expo Configuration
Add the plugin to your app.json or app.config.js:
{
"plugins": [
[
"rns-recplay",
{
"microphonePermission": "Allow microphone access for voice recording."
}
]
]
}🚀 Usage
🎙️ Start & Stop Recording
import Recplay from 'rns-recplay';
// Start recording
const fileName = await Recplay.startRecording({
fileName: "my_voice_note", // optional, defaults to rec_<timestamp>
shouldStopPlayback: true, // stop any playing audio first
duck: true, // lower other audio while recording
mixWithOthers: true, // mix with other audio sessions
useBT: false, // false = force built-in mic (recommended)
onSecondsUpdate: (seconds) => {
console.log(`Recorded: ${seconds}s`);
},
onVolumeUpdate: (db, normalized) => {
// db: raw decibel value (~-60 to 0)
// normalized: 0.0 to 1.0 — use this to drive a waveform visualizer
console.log(`Volume: ${normalized}`);
},
});
console.log("Recording started, file name:", fileName);
// Stop recording
const result = await Recplay.stopRecording();
console.log("URI:", result.uri); // file:///...path/to/file.m4a
console.log("Duration:", result.duration); // seconds, e.g. 5.4⏸️ Pause & Resume Recording
await Recplay.pauseRecording();
await Recplay.resumeRecording();🔊 Playback
Recplay.playAudio({
uri: "file:///path/to/audio.m4a",
shouldStopPrevious: true,
loop: false,
mixWithOthers: true,
duck: true,
callbacks: {
onStatus: (status) => {
// 'BUFFERING' | 'PLAYING' | 'PAUSED' | 'ENDED' | 'ERROR'
console.log("Status:", status);
if (status === 'PLAYING') {
// safe to seek or update UI
}
if (status === 'ENDED') {
// reset playhead
}
},
onProgress: (currentPosition, duration) => {
// fires every ~50ms while playing
console.log(`${currentPosition} / ${duration}`);
},
onPlaybackFinished: () => {
console.log("Playback finished");
},
}
});🎯 Seek, Toggle & Stop
// Seek to a specific time (sample-accurate)
Recplay.seekTo({ seconds: 30.5 });
// Toggle between play and pause
Recplay.togglePlayback();
// Stop playback and release resources
await Recplay.stopPlayback();🔐 Permissions
// Check current status
const status = await Recplay.checkPermission();
// Returns: 'granted' | 'denied' | 'blocked' | 'unavailable'
// Request permission
const result = await Recplay.requestPermission();
// Returns: 'granted' | 'denied'📚 API Reference
startRecording(options?)
| Parameter | Type | Default | Description |
|----------------------|------------|---------|---------------------------------------------------------------------|
| fileName | string | null | Output file name (without extension). Defaults to rec_<timestamp> |
| shouldStopPlayback | boolean | true | Stop any active playback before recording |
| duck | boolean | true | Lower volume of other audio while recording |
| mixWithOthers | boolean | true | Allow mixing with other active audio sessions |
| useBT | boolean | false | Use Bluetooth mic. false forces the built-in microphone |
| onSecondsUpdate | function | null | Called once per second with (seconds: number) |
| onVolumeUpdate | function | null | Called every 100ms with (db: number, normalized: number) |
Returns: Promise<string> — the file name (without extension)
stopRecording()
Stops the active recording, releases the microphone, and returns the saved file info.
Returns: Promise<{ uri: string, duration: number }>
| Field | Type | Description |
|------------|----------|-----------------------------------|
| uri | string | Full file URI, e.g. file:///... |
| duration | number | Duration in seconds, e.g. 5.4 |
pauseRecording()
Pauses the active recording session. Elapsed time is preserved accurately across pauses.
Returns: Promise<boolean>
resumeRecording()
Resumes a paused recording session.
Returns: Promise<boolean>
playAudio(options)
| Parameter | Type | Default | Description |
|----------------------|-----------|---------|-------------------------------------------|
| uri | string | — | Audio file URI or HTTP URL |
| shouldStopPrevious | boolean | false | Stop and release any previous playback |
| loop | boolean | false | Loop playback natively (gapless) |
| mixWithOthers | boolean | true | Mix with other active audio sessions |
| duck | boolean | false | Lower volume of other audio while playing |
| callbacks | object | {} | Event callbacks (see below) |
Callbacks
| Callback | Signature | Description |
|----------------------|-----------------------------------------------------------|-----------------------------------|
| onStatus | (status: string) => void | Player state changes (see below) |
| onProgress | (currentPosition: number, duration: number) => void | Fires every ~50ms while playing |
| onPlaybackFinished | () => void | Fires when non-looping audio ends |
stopPlayback()
Stops playback, removes all observers, and releases the player.
Returns: Promise<boolean>
togglePlayback()
Toggles between play and pause on the current player instance.
seekTo({ seconds })
Seeks to a precise position. Uses zero-tolerance seeking for sample accuracy.
| Parameter | Type | Description |
|-----------|----------|----------------------------|
| seconds | number | Target position in seconds |
checkPermission()
Returns: Promise<'granted' | 'denied' | 'blocked' | 'unavailable'>
| Status | Meaning |
|---------------|--------------------------------------------------------------------------|
| granted | Microphone is available and permitted |
| denied | Not asked yet (iOS) or dismissed once (Android) — can still request |
| blocked | User selected "Don't Allow" / "Never ask again" — redirect to Settings |
| unavailable | Hardware missing or OS-restricted |
requestPermission()
Triggers the native system permission dialog.
Returns: Promise<'granted' | 'denied'>
📌 Playback Status Values
| Status | When |
|-------------|-----------------------------------------------|
| BUFFERING | Loading / rebuffering (network or large file) |
| PLAYING | Audio is actively playing |
| PAUSED | Paused by user or audio focus loss |
| ENDED | Playback reached the end of the file |
| ERROR | Playback failed (bad URI, decode error, etc.) |
🛠️ Platform Support
| Platform | Supported | |------------------|-----------| | Android | ✅ | | iOS | ✅ | | Expo (Dev / EAS) | ✅ |
📝 Notes
- Recorded files are saved in the app's cache directory as
.m4a(AAC, 44.1kHz, 128kbps, mono) onVolumeUpdatefires at 100ms intervals — usenormalized(0.0–1.0) to drive waveform visualizer barsonSecondsUpdatefires once per second only on whole-second boundaries to avoid flooding JSuseBT: 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 modeseekTois safe to call immediately afteronStatus: 'PLAYING'fires- Calling
stopPlaybackcleans up all native observers — no memory leaks
📄 License
MIT
