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

react-native-sip-voice

v0.2.3

Published

Carrier-agnostic VoIP calling for React Native: SIP.js over WebSocket + react-native-webrtc audio, native CallKit (iOS) and ConnectionService (Android), and an optional WhatsApp-style call UI.

Readme

react-native-sip-voice

Carrier-agnostic voice calling for React Native and Expo. Bring your own SBC.

  • SIP over WebSocket via SIP.js, audio via react-native-webrtc.
  • Native call integration: CallKit on iOS, and a self-managed ConnectionService plus a foreground service on Android. Calls appear on the lock screen, in the car, and on Bluetooth devices, and they survive backgrounding.
  • Pluggable auth: SIP digest credentials, or a bearer token (RFC 8898) sent as a header or WebSocket query parameter.
  • Optional WhatsApp-style call UI (react-native-sip-voice/ui). It includes a minimize-to-banner mode and a photo of a famous landmark in the destination country (the London Eye for +44, the Eiffel Tower for +33, and 87 others).
  • Headless core with no RxJS. Fully typed, and 226 tests.
┌────────────── your app ──────────────┐
│  <SipVoiceProvider>   <CallOverlay/> │   react-native-sip-voice/ui (optional)
│        │                             │
│  SipVoiceClient ── Call ── hooks     │   core (headless)
│        │                             │
│  SignalingAdapter (SIP.js/WSS)       │──── wss:// ────► your SBC ──► PSTN / SIP trunk
│  react-native-webrtc (DTLS-SRTP)     │──── RTP/ICE ───►
│  NativeCallCoordinator               │
│        │                             │
│  Expo module: CallKit · PushKit ·    │
│  ConnectionService · FGS · audio     │
└──────────────────────────────────────┘

Documentation

Full guides with copy-paste examples live in docs/:

Getting started · Authentication · Calls · Hooks · Built-in UI · Native integration · Incoming calls & push · SBC setup · API reference · Testing · Troubleshooting

Requirements

| | | |---|---| | React Native | New Architecture (0.76+) | | Expo | SDK 53+ (bare or prebuild; not Expo Go) | | iOS | 15.1+ | | Android | API 24+ (ConnectionService on API 26+) | | Peer deps | react-native-webrtc; for the UI: expo-image, expo-blur, expo-linear-gradient, @expo/vector-icons, react-native-safe-area-context |

Your SBC must speak WebRTC

Browsers and react-native-webrtc only send DTLS-SRTP over ICE, never plain RTP. Your SBC, or a media relay in front of it, must support:

  • SIP over secure WebSocket (wss://, RFC 7118)
  • ICE (ICE-lite is fine), DTLS-SRTP, rtcp-mux, BUNDLE
  • Opus or PCMU/PCMA audio

This works with Kamailio or OpenSIPS + rtpengine, FreeSWITCH (mod_sofia WSS + mod_verto media), Asterisk (res_pjsip with webrtc=yes), AudioCodes, Ribbon, Oracle, and others.

Install

npx expo install react-native-sip-voice react-native-webrtc @config-plugins/react-native-webrtc
# for the built-in UI
npx expo install expo-image expo-blur expo-linear-gradient @expo/vector-icons react-native-safe-area-context

app.json:

{
  "expo": {
    "plugins": [
      "@config-plugins/react-native-webrtc",
      ["react-native-sip-voice", { "microphonePermission": "Used for voice calls", "voipPush": false }]
    ]
  }
}

Then run npx expo prebuild or npx expo run:ios / run:android.

The plugin adds the voip and audio background modes and the microphone text on iOS. Android permissions and services merge in from the library manifest.

Quick start

import { SipVoiceClient, SipVoiceProvider } from "react-native-sip-voice";
import { CallOverlay } from "react-native-sip-voice/ui";

const client = new SipVoiceClient({
  // Called on connect() and when a connection needs to be re-established. Fetch short-lived creds from your backend.
  credentialsProvider: async () => {
    const r = await fetch("https://api.example.com/sip-credentials").then((x) => x.json());
    return { type: "token", wsServer: r.wsServer, domain: r.domain, username: r.user, token: r.token };
  },
});

export default function App() {
  return (
    <SipVoiceProvider client={client} nativeConfig={{ appName: "My App" }}>
      <Navigation />
      <CallOverlay />
    </SipVoiceProvider>
  );
}

// anywhere
await client.newCall({
  destination: "+447700900123",
  callerId: "+15550001111",       // → X-Caller-Id
  clientState: btoa(JSON.stringify({ plan: "gold" })), // → X-Client-State
  headers: { "X-Tenant": "acme" },
});

Authentication

// SIP digest
{ type: "digest", wsServer: "wss://sbc.example.com:7443", domain: "sip.example.com",
  username: "1001", password: "secret" }

// Bearer token: Authorization: Bearer <token> on REGISTER and INVITE
{ type: "token", wsServer, domain, token }

// Custom header, and/or the token in the WebSocket URL
{ type: "token", wsServer, domain, token, headerName: "X-Auth-Token", wsQueryParam: "access_token" }

Common options: iceServers (STUN/TURN), register (send REGISTER; not needed for outgoing-only apps), displayName, userAgentString, traceSip.

Client API

const client = new SipVoiceClient({
  debug?: boolean,
  nativeCallUI?: boolean,            // CallKit / ConnectionService (default true)
  callerIdHeader?: string,           // default "X-Caller-Id" (e.g. "P-Asserted-Identity")
  clientStateHeader?: string,        // default "X-Client-State"
  dtmfMode?: "auto" | "rfc2833" | "info", // default auto: RTP, then falls back to SIP INFO
  maxReconnectAttempts?: number,     // default 5, exponential back-off
  outgoingCallTimeoutMs?: number,    // default 60000
  credentialsProvider?: () => Promise<SipCredentials>,
});

client.connect(creds?)      client.disconnect()      client.newCall(opts)
client.connectionState$     client.calls$            client.activeCall$
client.on("incomingCall" | "callEnded" | "error", fn)
client.handleNetworkChange()   // the provider calls this when the app returns to the foreground

call.hangup()  call.answer()  call.reject()  call.mute()/unmute()/toggleMute()
call.hold()/resume()/toggleHold()  call.sendDtmf("1")
call.callState$  call.isMuted$  call.isOnHold$  call.durationSeconds  call.endReason

CallState: CONNECTING → RINGING → ACTIVE ⇄ HELD, RECONNECTING, ENDED, FAILED.

Hooks

useSipVoice(), useConnectionState(), useActiveCall(), useCalls(), useCallState(call), useCallDuration(call), useAudioRoute(). The last one returns { route, available, setRoute, toggleSpeaker, isSpeakerOn }, where route is earpiece, speaker, Bluetooth, or headset.

UI

<CallOverlay /> shows the full-screen call. The user can minimize it to a "Tap to return to call" banner, and it pops back to full screen when the call ends.

<CallOverlay
  resolveDisplayName={async (call) => lookupContact(call.destination)}
  phoneNumberForCall={(call) => call.metadata.dialedNumber ?? call.destination}
  landmarkResolver={(number, match) => match?.iso === "GB" ? { imageUrl: myCdn("london.jpg") } : undefined}
  subtitle={() => "My App"}
  theme={{ accent: "#25D366" }}
  labels={{ end: "Raccrocher" }}
  allowMinimize
/>

Building blocks are exported too: ActiveCallScreen, IncomingCallScreen, MinimizedCallBanner, DialPad, Avatar, ControlButton, LandmarkBackground, resolveLandmark, resolveCountry.

Landmark images

Images are not bundled. They load from Wikimedia and are cached on disk with expo-image. If a photo can't load (for example when offline), the screen shows a country-tinted gradient instead.

Every image is free-licensed (CC0, public domain, CC BY, CC BY-SA, FAL, or KOGL). Authors and licences are listed in ATTRIBUTION.md, and a one-line credit appears on the call screen, as CC licences require. To serve images from your own CDN, pass landmarkResolver.

To regenerate the data:

node scripts/fetch-landmarks.mjs && node scripts/apply-overrides.mjs && node scripts/build-attribution.mjs

Background behaviour

| | iOS | Android | |---|---|---| | System call UI | CallKit (lock screen, Recents, CarPlay, Watch) | Self-managed ConnectionService (car, Bluetooth, wearables) | | While backgrounded | voip + audio background modes | Foreground service (microphone|phoneCall) with a CallStyle ongoing-call notification (chronometer, hang-up) | | Audio session | WebRTC manual audio, enabled on didActivate (fixes one-way audio when answering from the lock screen) | Telecom sets MODE_IN_COMMUNICATION; AudioManager is the fallback | | System actions → JS | end, answer, mute, hold, DTMF | end, answer, hold, mute (from audio state), DTMF |

Incoming calls (push-ready)

  • iOS: set "voipPush": true in the plugin options. PushKit then registers at launch, and you can send the token (getNativeCallUI().getVoipPushToken() or the voipPushToken event) to your backend. Each VoIP push must carry call_id, caller_number, and optionally caller_name and call_uuid. The native side reports the push to CallKit immediately, as iOS requires. When the SIP INVITE arrives with X-Call-Id: <call_id>, it attaches to the same call.
  • Android: in your FCM handler, call getNativeCallUI().reportIncomingCall(uuid, number, name) and register the call with client.registerPushCall(uuid, payload).
  • Answering from the system UI before the INVITE arrives is handled: the answer is applied as soon as the call exists.

Custom signalling

SignalingAdapter is a small interface. Implement it to use a different SIP stack, or anything else that can reach your SBC, and pass it as new SipVoiceClient(opts, { adapter }).

Testing

npm test            # 226 tests: core, SIP adapter, native coordinator, hooks, UI, config plugin
npm run test:coverage
npm run typecheck

Licence

MIT. Landmark photos are the property of their authors and used under the licences in ATTRIBUTION.md.