npm package: @superapp_men/voice-recorder-capacitor
Description: Voice recorder with checkpoint support for Capacitor-based SuperApp Partner Apps
Author: Your Organization
Latest version: 1.1.3

# @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

```bash
npm install @superapp_men/voice-recorder-capacitor
```

## How 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)

```typescript
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

```typescript
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 checkpoints
```

### 3. React Example

```tsx
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)               |
| `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`

```typescript
{
  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
}
```

### `Checkpoint`

```typescript
{
  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;
  };
}
```

### Enums

```typescript
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):**
```typescript
{ format: AudioFormat.WAV, sampleRate: 16000, bitDepth: 16, channels: 1 }
```

**High quality:**
```typescript
{ 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:

```typescript
import {
  MessageType,
  RecorderState,
  type SuperAppMessage,
  type StartRecordingPayload,
  type RecordingResult,
  type Checkpoint,
} from "@superapp_men/voice-recorder-capacitor/superapp";
```

The SuperApp must:
1. Listen for `MessageType.START_RECORDING` via `postMessage`
2. Start the native Capacitor recording + an AudioWorklet for chunking
3. Push `MessageType.CHECKPOINT_PUSH` messages to the iframe automatically
4. On `MessageType.STOP_RECORDING`, stop everything and return the full `RecordingResult`

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:
```typescript
const recorder = new VoiceRecorder({ debug: true });
```

## License

MIT

## Support

- Email: h.afifi@alexsys.solutions