npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cloudfort/callum-voice

v0.0.4

Published

Real-time voice chat SDK for React Native apps

Readme

Callum.Voice React Native SDK — Real-Time Voice Chat Library

A React Native plugin for cross-platform real-time voice communication over a Go server (phonil-opus).
Works on Android and iOS.
All microphone capture, audio playback, WebSocket connection, multi-peer audio mixing, speaking detection, and room management logic is implemented in this plugin.


Table of Contents


Architecture

┌──────────────────────────────────────────────────────────────┐
│            Callum.Voice React Native SDK                     │
│                                                              │
│  ┌─────────────────────┐     ┌──────────────────────────┐   │
│  │   VoiceClient (TS)   │────▶│   Native Modules         │   │
│  │                      │◀────│                          │   │
│  │ • WebSocket (native) │     │  Android: AudioRecord/   │   │
│  │ • Room Management    │     │         AudioTrack       │   │
│  │ • Peer Tracking      │     │  iOS:     AVAudioEngine  │   │
│  │ • Speaking Detection │     └──────────────────────────┘   │
│  │ • Auto Reconnect     │                  ▲                 │
│  └──────────┬───────────┘                  │                 │
│             │ WebSocket (PCM 16-bit mono)  │                 │
│             ▼                              │                 │
│  ┌─────────────────────┐                   │                 │
│  │   Go Server          │── PCM Audio ─────┘                 │
│  │  (phonil-opus)       │                                    │
│  └─────────────────────┘                                     │
└──────────────────────────────────────────────────────────────┘

Data Flow

  Microphone ──▶ NativeModule ──(Int16[])──▶ JS VoiceClient ──(WebSocket)──▶ Server
  Speaker    ◀── NativeModule ◀──(Int16[])──◀ JS VoiceClient ◀──(WebSocket)──◀ Server

File Structure

callum-react-native/
├── src/
│   ├── index.ts                   # Library exports
│   ├── VoiceClientState.ts        # Connection state enum
│   ├── VoiceConfig.ts             # Configuration interface
│   ├── VoiceEventListener.ts      # Event callback interface
│   └── VoiceClient.ts             # Core client logic
├── android/
│   ├── build.gradle               # Android build config
│   └── src/main/
│       ├── AndroidManifest.xml    # Permissions
│       └── java/ir/cloudfort/callum/
│           ├── CallumVoiceModule.kt   # Native audio module
│           └── CallumVoicePackage.kt  # Package registration
├── ios/
│   ├── CallumVoiceModule.swift        # Native audio module
│   ├── CallumVoiceModule-Bridging-Header.h  # ObjC bridge
│   └── CallumVoiceBridge.m           # Bridge implementation
├── callum-voice.podspec           # iOS CocoaPods spec
├── package.json                   # npm package config
├── tsconfig.json                  # TypeScript config
└── README.md                      # This file

Communication Protocol

WebSocket Connection

ws(s)://{server}/ws?room=__lobby__&peer={peerId}&api_key={apiKey}

Binary Packet Format

┌──────────┬──────────┬──────────┬──────────┬──────────────┐
│ RoomLen  │ RoomID   │ PeerLen  │ PeerID   │ PCM Audio    │
│ (1 byte) │ (N bytes)│ (1 byte) │ (N bytes)│ (variable)   │
└──────────┴──────────┴──────────┴──────────┴──────────────┘

Audio Format

  • PCM 16-bit signed integer, mono, 48000 Hz
  • ~960 samples per 20ms frame (~768 kbps)

JSON Control Messages

// Join room
{"type": "join", "room": "room_id", "peer": "peer_id", "sampleRate": 48000}

// UDP token (server → client)
{"type": "udp_token", "token": "..."}

Installation

1. Install Package

npm install callum-voice
# or
yarn add callum-voice

2. iOS — Install Pods

cd ios && pod install && cd ..

3. Android — Register Package

In MainApplication.kt:

import ir.cloudfort.callum.CallumVoicePackage

override fun getPackages(): List<ReactPackage> {
    return listOf(
        MainReactPackage(),
        CallumVoicePackage()  // ← Add this
    )
}

4. Platform Permissions

Android (android/app/src/main/AndroidManifest.xml)

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

iOS (ios/Runner/Info.plist)

<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice chat</string>

Quick Start

import { VoiceClient, createVoiceConfig } from 'callum-voice';

// 1. Create config
const config = createVoiceConfig({
  server: 'callem.cloudfort.ir',
  apiKey: 'vc_token',
  peerId: 'user_123',
});

// 2. Create client
const client = new VoiceClient(config);

// 3. Set listener
client.setListener({
  onConnected: () => console.log('Connected!'),
  onPeerSpeaking: (peerId) => console.log(`${peerId} is speaking`),
});

// 4. Connect
await client.connect();

// 5. Join a room
await client.joinRoom('game_room_1');

// 6. Enable microphone
await client.enableMic();

Complete Example

import React, { useEffect, useState, useRef, useCallback } from 'react';
import {
  View,
  Text,
  FlatList,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import {
  VoiceClient,
  VoiceClientState,
  VoiceEventListener,
  createVoiceConfig,
} from 'callum-voice';

const STATE_LABELS: Record<VoiceClientState, string> = {
  [VoiceClientState.Disconnected]: 'Disconnected',
  [VoiceClientState.Connecting]: 'Connecting...',
  [VoiceClientState.Connected]: 'Connected',
  [VoiceClientState.InRoom]: 'In Room',
  [VoiceClientState.Reconnecting]: 'Reconnecting...',
};

export default function VoiceChatScreen() {
  const clientRef = useRef<VoiceClient | null>(null);
  const [state, setState] = useState<VoiceClientState>(VoiceClientState.Disconnected);
  const [peers, setPeers] = useState<string[]>([]);
  const [speakingPeers, setSpeakingPeers] = useState<Set<string>>(new Set());
  const [micEnabled, setMicEnabled] = useState(false);
  const [speakerEnabled, setSpeakerEnabled] = useState(false);

  useEffect(() => {
    const config = createVoiceConfig({
      server: 'callem.cloudfort.ir',
      apiKey: 'vc_token',
      peerId: `user_${Date.now()}`,
    });

    const client = new VoiceClient(config);
    clientRef.current = client;

    const listener: VoiceEventListener = {
      onConnected: () => console.log('Connected'),
      onDisconnected: () => {
        setPeers([]);
        setSpeakingPeers(new Set());
      },
      onReconnecting: (attempt) => console.log(`Reconnecting #${attempt}`),
      onAuthFailed: (reason) => Alert.alert('Auth Failed', reason),
      onRoomJoined: (roomId) => console.log(`Joined ${roomId}`),
      onRoomLeft: () => {
        setPeers([]);
        setSpeakingPeers(new Set());
      },
      onPeerJoined: (peerId) => setPeers(prev => [...prev, peerId]),
      onPeerLeft: (peerId) => {
        setPeers(prev => prev.filter(p => p !== peerId));
        setSpeakingPeers(prev => {
          const next = new Set(prev);
          next.delete(peerId);
          return next;
        });
      },
      onPeerSpeaking: (peerId) =>
        setSpeakingPeers(prev => new Set(prev).add(peerId)),
      onPeerStopped: (peerId) =>
        setSpeakingPeers(prev => {
          const next = new Set(prev);
          next.delete(peerId);
          return next;
        }),
      onMicEnabled: () => setMicEnabled(true),
      onMicDisabled: () => setMicEnabled(false),
      onSpeakerEnabled: () => setSpeakerEnabled(true),
      onSpeakerDisabled: () => setSpeakerEnabled(false),
      onError: (error) => Alert.alert('Voice Error', error.message),
      onStateChanged: (newState) => setState(newState),
    };

    client.setListener(listener);
    client.connect().then(() => client.joinRoom('game_room_1'));

    return () => {
      client.dispose();
      clientRef.current = null;
    };
  }, []);

  const handleToggleMic = useCallback(async () => {
    const client = clientRef.current;
    if (!client) return;
    if (micEnabled) {
      client.disableMic();
    } else {
      await client.enableMic();
    }
  }, [micEnabled]);

  const handleToggleSpeaker = useCallback(() => {
    const client = clientRef.current;
    if (!client) return;
    if (speakerEnabled) {
      client.disableSpeaker();
    } else {
      client.enableSpeaker();
    }
  }, [speakerEnabled]);

  return (
    <View style={styles.container}>
      {/* Status Bar */}
      <View style={styles.statusBar}>
        <Text style={styles.statusText}>{STATE_LABELS[state]}</Text>
      </View>

      {/* Controls */}
      <View style={styles.controls}>
        <TouchableOpacity
          style={[styles.button, micEnabled && styles.buttonActive]}
          onPress={handleToggleMic}
        >
          <Text style={styles.buttonText}>
            {micEnabled ? '🎤 Mute' : '🎤 Unmute'}
          </Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.button, speakerEnabled && styles.buttonActive]}
          onPress={handleToggleSpeaker}
        >
          <Text style={styles.buttonText}>
            {speakerEnabled ? '🔊 Speaker Off' : '🔇 Speaker On'}
          </Text>
        </TouchableOpacity>
      </View>

      {/* Peers List */}
      <FlatList
        data={peers}
        keyExtractor={(item) => item}
        renderItem={({ item }) => {
          const isSpeaking = speakingPeers.has(item);
          return (
            <View style={styles.peerRow}>
              <Text style={styles.peerIcon}>{isSpeaking ? '🟢' : '⚪'}</Text>
              <View style={styles.peerInfo}>
                <Text style={styles.peerName}>{item}</Text>
                <Text style={styles.peerStatus}>
                  {isSpeaking ? 'Speaking...' : 'Silent'}
                </Text>
              </View>
            </View>
          );
        }}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No peers in room</Text>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#1a1a2e' },
  statusBar: {
    padding: 16,
    backgroundColor: '#16213e',
    alignItems: 'center',
  },
  statusText: { color: '#e94560', fontSize: 18, fontWeight: 'bold' },
  controls: {
    flexDirection: 'row',
    justifyContent: 'center',
    padding: 16,
    gap: 16,
  },
  button: {
    paddingHorizontal: 24,
    paddingVertical: 12,
    backgroundColor: '#0f3460',
    borderRadius: 8,
  },
  buttonActive: { backgroundColor: '#e94560' },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  peerRow: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#16213e',
  },
  peerIcon: { fontSize: 24, marginRight: 12 },
  peerInfo: { flex: 1 },
  peerName: { color: '#fff', fontSize: 16, fontWeight: '500' },
  peerStatus: { color: '#888', fontSize: 13 },
  emptyText: { color: '#888', textAlign: 'center', marginTop: 40, fontSize: 16 },
});

API Reference

VoiceClient

The main class for managing voice connections.

Constructor

new VoiceClient(config: VoiceConfig)

| Parameter | Type | Description | |-----------|------|-------------| | config | VoiceConfig | Connection and audio configuration |

Properties

| Property | Type | Description | |----------|------|-------------| | state | VoiceClientState | Current connection state (read-only) | | isConnected | boolean | Whether connected to server | | isInRoom | boolean | Whether joined a room | | isMicEnabled | boolean | Whether microphone is active | | isSpeakerEnabled | boolean | Whether speaker output is active | | currentRoomId | string \| null | Current room ID | | peerIds | string[] | List of peer IDs in the current room |

Methods

| Method | Returns | Description | |--------|---------|-------------| | setListener(listener) | void | Set the event listener | | connect() | Promise<void> | Connect to the voice server | | disconnect() | void | Disconnect from the server | | joinRoom(roomId) | Promise<void> | Join a voice room | | leaveRoom() | Promise<void> | Leave the current room | | enableMic() | Promise<void> | Enable microphone (must be in room) | | disableMic() | void | Disable microphone | | toggleMic() | Promise<boolean> | Toggle mic, returns new state | | enableSpeaker() | void | Enable speaker output | | disableSpeaker() | void | Disable speaker output | | toggleSpeaker() | boolean | Toggle speaker, returns new state | | isPeerSpeaking(peerId) | boolean | Check if a specific peer is speaking | | getPeerSpeakingInfo(peerId) | {speaking, rms} | Get peer speaking state with RMS level | | sendAudio(samples) | void | Send captured audio to server (from native) | | dispose() | void | Cleanup and release resources |


VoiceConfig

Configuration for connecting to the voice chat service.

interface VoiceConfig {
  server: string;
  apiKey: string;
  peerId: string;
  useTls?: boolean;
  sampleRate: number;
  autoReconnect: boolean;
  maxReconnectAttempts: number;
  reconnectDelayMs: number;
  echoCancellation: boolean;
  noiseSuppression: boolean;
  autoGainControl: boolean;
}

createVoiceConfig Helper

function createVoiceConfig(overrides: Partial<VoiceConfig>): VoiceConfig

Creates a VoiceConfig with sensible defaults, merged with your overrides.

| Property | Type | Default | Description | |----------|------|---------|-------------| | server | string | '' | Server address (e.g. "callem.cloudfort.ir"). Do NOT include scheme or port. | | apiKey | string | '' | API key from account registration (format: vc_live_...) | | peerId | string | '' | Unique identifier for this peer/user | | useTls | boolean? | undefined | Use WSS instead of WS. Auto-detected when undefined. | | sampleRate | number | 48000 | Audio sample rate in Hz | | autoReconnect | boolean | true | Automatically reconnect on disconnect | | maxReconnectAttempts | number | 5 | Maximum reconnection attempts | | reconnectDelayMs | number | 2000 | Delay between reconnection attempts (ms) | | echoCancellation | boolean | true | Enable platform echo cancellation | | noiseSuppression | boolean | true | Enable platform noise suppression | | autoGainControl | boolean | true | Enable platform automatic gain control |


VoiceEventListener

Interface for receiving voice client events. All methods are optional.

interface VoiceEventListener {
  onConnected?(): void;
  onDisconnected?(): void;
  onReconnecting?(attempt: number): void;
  onAuthFailed?(reason: string): void;
  onRoomJoined?(roomId: string): void;
  onRoomLeft?(roomId: string): void;
  onPeerJoined?(peerId: string): void;
  onPeerLeft?(peerId: string): void;
  onPeerSpeaking?(peerId: string): void;
  onPeerStopped?(peerId: string): void;
  onMicEnabled?(): void;
  onMicDisabled?(): void;
  onSpeakerEnabled?(): void;
  onSpeakerDisabled?(): void;
  onError?(error: Error): void;
  onStateChanged?(newState: VoiceClientState): void;
}

Events

| Method | Parameters | Description | |--------|-----------|-------------| | onConnected | — | Connection established | | onDisconnected | — | Connection lost | | onReconnecting | attempt — current attempt number | Reconnection in progress | | onAuthFailed | reason — failure description | Authentication failed | | onRoomJoined | roomId — joined room ID | Successfully joined a room | | onRoomLeft | roomId — left room ID | Left a room | | onPeerJoined | peerId — new peer's ID | A peer joined the room | | onPeerLeft | peerId — departing peer's ID | A peer left the room | | onPeerSpeaking | peerId — speaking peer's ID | Peer started speaking | | onPeerStopped | peerId — silent peer's ID | Peer stopped speaking | | onMicEnabled | — | Microphone enabled | | onMicDisabled | — | Microphone disabled | | onSpeakerEnabled | — | Speaker output enabled | | onSpeakerDisabled | — | Speaker output disabled | | onError | error — Error object | An error occurred | | onStateChanged | newState — VoiceClientState | Connection state changed |


Native Modules

Android (Kotlin)

The Android native module at android/src/main/java/ir/cloudfort/callum/CallumVoiceModule.kt provides:

| Method | Description | |--------|-------------| | startAudioCapture() | Start microphone recording via AudioRecord | | stopAudioCapture() | Stop microphone recording | | startAudioPlayback() | Start audio playback via AudioTrack | | stopAudioPlayback() | Stop audio playback | | enqueuePeerAudio(peerId, samples) | Write PCM samples to AudioTrack | | setSpeakerphoneOn(on) | Route audio to speaker/earpiece | | configureAudioSession() | Set MODE_IN_COMMUNICATION |

Events sent to JavaScript:

  • onAudioCaptured — Int16 array of captured PCM samples
  • onAudioError — Error message string

iOS (Swift)

The iOS native module at ios/CallumVoiceModule.swift provides:

| Method | Description | |--------|-------------| | startAudioCapture() | Start microphone capture via AVAudioEngine | | stopAudioCapture() | Stop microphone capture | | startAudioPlayback() | Start audio playback via AVAudioEngine | | stopAudioPlayback() | Stop audio playback | | enqueuePeerAudio(peerId, samples) | Schedule PCM buffer for playback | | setSpeakerphoneOn(on) | Override output audio port | | configureAudioSession() | Configure AVAudioSession for voice chat |

Events sent to JavaScript:

  • onAudioCaptured — Int16 array of captured PCM samples
  • onAudioError — Error message string

Permissions

Android

| Permission | Required | Description | |-----------|----------|-------------| | INTERNET | Yes | WebSocket connection to server | | RECORD_AUDIO | Yes | Microphone capture | | MODIFY_AUDIO_SETTINGS | Yes | Audio routing control | | ACCESS_NETWORK_STATE | Recommended | Network change detection |

Request at runtime:

import { PermissionsAndroid } from 'react-native';

const granted = await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
  { title: 'Microphone', message: 'Needed for voice chat' }
);

iOS

| Key | Required | Description | |-----|----------|-------------| | NSMicrophoneUsageDescription | Yes | Microphone access reason |


Important Notes

Connection Flow

  1. Create VoiceConfig with server, API key, and peer ID
  2. Create VoiceClient with config
  3. Set a VoiceEventListener
  4. Call connect() — establishes WebSocket connection
  5. Call joinRoom(roomId) — joins a voice room
  6. Call enableMic() — starts sending audio
  7. Call enableSpeaker() — starts receiving audio

Audio Format

  • PCM 16-bit signed integer, mono, 48000 Hz
  • 960 samples per 20ms frame
  • ~768 kbps bandwidth per peer

Speaking Detection

  • RMS threshold: 0.012 (normalized)
  • Check interval: 80ms
  • Window: last 480 samples (~10ms at 48kHz)

Ring Buffer

  • Capacity: 96,000 samples (~2 seconds at 48kHz)
  • Oldest data is overwritten when full

Auto-Reconnect

  • Saves current room and mic state before disconnect
  • Attempts reconnection up to maxReconnectAttempts times
  • Restores room and mic state on successful reconnection
  • Delay between attempts: reconnectDelayMs (default 2000ms)

Authentication

  • API key format: vc_live_ followed by 32 hex characters
  • Sent as query parameter in WebSocket URL
  • Auth failures return close code 1008 or 4001

Multi-Peer Audio Mixing

  • Volume scaling: min(1.0, 0.8 / peerCount)
  • Prevents clipping when multiple peers speak simultaneously

Thread Safety

  • WebSocket events handled on the JS thread
  • Native audio capture/playback on dedicated threads
  • Peer state managed in JS single-threaded context

Cleanup

  • Always call dispose() when done to release resources
  • disconnect() disables auto-reconnect before cleaning up
  • Native audio resources are released in onCatalystInstanceDestroy / deinit

License

Copyright © Cloudfort. All Rights Reserved.