@superapp_men/voice-recorder-capacitor
v1.1.4
Published
Voice recorder with checkpoint support for Capacitor-based SuperApp Partner Apps
Downloads
301
Maintainers
Readme
@superapp_men/voice-recorder-capacitor
Voice recorder for SuperApp partner apps. Records audio through the SuperApp's native layer (Capacitor) with automatic checkpoint streaming — start once, receive audio chunks every second without interrupting the recording.
Install
npm install @superapp_men/voice-recorder-capacitorHow It Works
Partner App (iframe) SuperApp (Capacitor host)
| |
|-- startRecording(config) --> |-- starts native mic
| |-- starts AudioWorklet chunking
| |
| <-- checkpoint #1 (push) --- | (every 1s, automatic)
| <-- checkpoint #2 (push) --- |
| <-- checkpoint #3 (push) --- |
| |
|-- stopRecording -----------> |-- stops native mic
| <-- final full recording --- |- The native recording runs continuously — never stopped for checkpoints.
- An AudioWorklet captures raw PCM at the requested sample rate and pushes WAV chunks automatically.
- The partner app receives checkpoints via events — no polling, no manual calls.
Quick Start
1. Simple Recording (no checkpoints)
import { VoiceRecorder, AudioFormat, SampleRate } from "@superapp_men/voice-recorder-capacitor";
const recorder = new VoiceRecorder({ timeout: 10000, debug: true });
// Request permission
const permission = await recorder.requestPermission();
if (permission !== "granted") return;
// Record
await recorder.startRecording({
audioConfig: {
format: AudioFormat.WAV,
sampleRate: SampleRate.SR_16000,
bitDepth: 16,
channels: 1,
},
});
// ... user speaks ...
const result = await recorder.stopRecording();
// result.audioData → base64-encoded WAV at 16kHz/16bit/mono
// result.duration → total ms
// result.audioConfig → { format: "wav", sampleRate: 16000, bitDepth: 16, channels: 1 }2. Recording with Auto Checkpoints
const recorder = new VoiceRecorder({ timeout: 10000, debug: true });
// Listen for checkpoints — they arrive automatically, no manual calls needed
recorder.on("checkpointCreated", ({ checkpoint }) => {
console.log(`Chunk #${checkpoint.index}`, {
segmentDuration: checkpoint.segmentDuration, // ~1000ms
sampleRate: checkpoint.audioConfig.sampleRate, // 16000
size: checkpoint.size,
});
// Send checkpoint.audioData to your STT API, save it, etc.
});
await recorder.requestPermission();
await recorder.startRecording({
isCheckpoints: true, // enable auto checkpoints
checkpointInterval: 1000, // one chunk every 1 second (min: 500ms)
maxDuration: 120_000, // max 2 minutes
audioConfig: {
format: AudioFormat.WAV,
sampleRate: SampleRate.SR_16000,
bitDepth: 16,
channels: 1,
},
});
// Checkpoints stream in automatically every 1s via the "checkpointCreated" event.
// No need to call createCheckpoint() — just listen.
// When done:
const result = await recorder.stopRecording();
// result.audioData → full recording (from native plugin, converted to WAV)
// result.checkpoints → array of all checkpoint objects
// result.checkpointCount → number of checkpoints3. React Example
import { useEffect, useState } from "react";
import {
VoiceRecorder,
RecorderState,
AudioFormat,
SampleRate,
formatDuration,
type RecordingResult,
type Checkpoint,
} from "@superapp_men/voice-recorder-capacitor";
function AudioRecorder() {
const [recorder] = useState(() => new VoiceRecorder({ timeout: 10000, debug: true }));
const [state, setState] = useState(RecorderState.IDLE);
const [duration, setDuration] = useState(0);
const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([]);
const [recording, setRecording] = useState<RecordingResult | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const unsubs = [
recorder.on("stateChange", ({ state }) => setState(state)),
recorder.on("progress", ({ duration }) => setDuration(duration)),
recorder.on("checkpointCreated", ({ checkpoint }) => {
setCheckpoints((prev) => [...prev, checkpoint]);
}),
recorder.on("error", ({ message }) => setError(message)),
];
return () => { unsubs.forEach((u) => u()); recorder.destroy(); };
}, [recorder]);
const isRecording = state === RecorderState.RECORDING;
const start = async () => {
setError(null);
setRecording(null);
setCheckpoints([]);
const p = await recorder.requestPermission();
if (p !== "granted") { setError("Permission denied"); return; }
await recorder.startRecording({
isCheckpoints: true,
checkpointInterval: 1000,
audioConfig: {
format: AudioFormat.WAV,
sampleRate: SampleRate.SR_16000,
bitDepth: 16,
channels: 1,
},
});
};
const stop = async () => {
const result = await recorder.stopRecording();
setRecording(result);
};
return (
<div>
<p>State: {state} | Duration: {formatDuration(duration)}</p>
{error && <p style={{ color: "red" }}>{error}</p>}
<button onClick={start} disabled={isRecording}>Start</button>
<button onClick={stop} disabled={!isRecording}>Stop</button>
{/* Live checkpoints */}
{checkpoints.map((cp) => (
<div key={cp.id}>
<p>Checkpoint #{cp.index} — {cp.audioConfig.sampleRate}Hz, {cp.segmentDuration}ms</p>
<audio controls src={`data:audio/wav;base64,${cp.audioData}`} />
</div>
))}
{/* Final result */}
{recording && (
<div>
<p>Done — {formatDuration(recording.duration)}, {recording.checkpointCount} checkpoints</p>
<audio controls src={`data:audio/wav;base64,${recording.audioData}`} />
</div>
)}
</div>
);
}API
new VoiceRecorder(config?)
| Option | Type | Default | Description |
| --------- | --------- | ------- | ------------------------ |
| timeout | number | 5000 | Request timeout (ms) |
| debug | boolean | false | Enable console logging |
Methods
| Method | Returns | Description |
| --------------------- | -------------------------- | ---------------------------------------------- |
| isAvailable() | Promise<boolean> | Can the device record? |
| checkPermission() | Promise<PermissionStatus> | Current mic permission status |
| requestPermission() | Promise<PermissionStatus> | Ask the user for mic access |
| startRecording(config?) | Promise<void> | Start recording with optional config |
| stopRecording() | Promise<RecordingResult> | Stop and get the full result |
| createCheckpoint() | Promise<Checkpoint> | On-demand checkpoint (rarely needed with auto mode) |
| getStatus() | Promise<RecordingStatus> | Current status from the SuperApp |
| getState() | RecorderState | Local state |
| isCurrentlyRecording() | boolean | Quick check |
| getDuration() | number | Elapsed ms since start |
| getCheckpoints() | Checkpoint[] | All checkpoints so far |
| on(event, callback) | () => void | Subscribe (returns unsubscribe fn) |
| off(event, callback) | void | Unsubscribe |
| destroy() | void | Cleanup everything |
startRecording(config)
| Option | Type | Default | Description |
| ------------------- | ------------- | -------- | ---------------------------------------------- |
| isCheckpoints | boolean | false | Enable auto checkpoint streaming |
| checkpointInterval| number | 1000 | Ms between checkpoints (min 500) |
| useModelAi | boolean | false | Enable/disable AI model processing |
| maxDuration | number | — | Auto-stop after this many ms |
| audioConfig.format| AudioFormat | WAV | AudioFormat.WAV or AudioFormat.PCM |
| audioConfig.sampleRate | number | 16000 | Hz (8000–48000) |
| audioConfig.bitDepth | number | 16 | 8, 16, 24, or 32 |
| audioConfig.channels | number | 1 | 1 (mono) or 2 (stereo) |
Events
| Event | Payload | Description |
| ------------------- | ------------------------------------ | ---------------------------------- |
| stateChange | { state, previousState } | Recorder state changed |
| recordingStarted | { sessionId, config } | Recording began |
| recordingStopped | { result: RecordingResult } | Recording ended |
| checkpointCreated | { checkpoint: Checkpoint } | A chunk arrived (auto or manual) |
| progress | { duration, checkpointCount } | Fires every 500ms while recording |
| error | { code, message } | Something went wrong |
RecordingResult
{
audioData: string; // base64 WAV of the full recording
duration: number; // total ms
size: number; // bytes
timestamp: number; // when recording finished
audioConfig: { // actual output config
format: string;
sampleRate: number;
bitDepth: number;
channels: number;
};
checkpointCount: number;
checkpoints?: Checkpoint[]; // present if isCheckpoints was true
modelAiResult?: any; // AI model output (if enabled)
}Checkpoint
{
id: string; // unique ID
index: number; // 1-based
audioData: string; // base64 WAV of this segment
duration: number; // elapsed ms from recording start
segmentDuration: number; // duration of this chunk in ms
size: number; // bytes
timestamp: number;
audioConfig: { // matches requested config
format: string;
sampleRate: number;
bitDepth: number;
channels: number;
};
modelAiResult?: any; // AI model output for this checkpoint (if enabled)
}Enums
enum RecorderState {
IDLE, REQUESTING_PERMISSION, READY, RECORDING, PAUSED, STOPPED, ERROR
}
enum AudioFormat { WAV = "wav", PCM = "pcm" }
enum SampleRate {
SR_8000 = 8000, SR_11025 = 11025, SR_16000 = 16000,
SR_22050 = 22050, SR_44100 = 44100, SR_48000 = 48000
}
type PermissionStatus = "granted" | "denied" | "prompt" | "unknown";Recommended Configs
Voice / STT (low bandwidth):
{ format: AudioFormat.WAV, sampleRate: 16000, bitDepth: 16, channels: 1 }High quality:
{ format: AudioFormat.WAV, sampleRate: 44100, bitDepth: 16, channels: 2 }For SuperApp Developers
If you're building the SuperApp side that handles these messages, import from the /superapp entry point:
import {
MessageType,
RecorderState,
type SuperAppMessage,
type StartRecordingPayload,
type RecordingResult,
type Checkpoint,
} from "@superapp_men/voice-recorder-capacitor/superapp";The SuperApp must:
- Listen for
MessageType.START_RECORDINGviapostMessage - Start the native Capacitor recording + an AudioWorklet for chunking
- Push
MessageType.CHECKPOINT_PUSHmessages to the iframe automatically - On
MessageType.STOP_RECORDING, stop everything and return the fullRecordingResult
See VoiceRecorderPackageService.ts for the reference implementation.
Troubleshooting
"Already recording" errors — The old start/stop checkpoint approach caused this. v1.1.0 uses continuous recording with AudioWorklet chunking — this error should no longer occur.
Checkpoints not arriving — Make sure isCheckpoints: true is in the config. Listen to "checkpointCreated" events — checkpoints are pushed automatically.
Wrong sample rate in checkpoints — The SuperApp creates an AudioContext at the requested rate. If the device can't honour it, a lightweight resample is applied. Check checkpoint.audioConfig.sampleRate to verify.
Debug mode — Pass debug: true to see all bridge messages:
const recorder = new VoiceRecorder({ debug: true });License
MIT
Support
- Email: [email protected]
