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

sip-phone-lib

v0.1.8

Published

SIP softphone React component library built on SIP.js

Readme

sip-phone-lib

An embeddable WebRTC softphone for React. Drop a single component into any web portal to get a floating dialer (FAB), incoming-call screen, in-call controls, DTMF keypad, blind transfer, attended add-call, device selection, call history, and a self-contained ringtone — all driven by SIP.js over a WebSocket SIP proxy.

The UI is rendered as compact 320×480 cards (light/dark themes) with no external CSS or asset dependencies. Everything — including the ringtone — is generated in code.


Features

  • 📞 Place & receive calls over SIP/WebRTC (audio).
  • 🟢 Floating widget (FAB) that opens a dialer and auto-opens for incoming/active calls.
  • ⌨️ Dial pad with + key, keyboard input, ripple feedback, and collapsible call history.
  • 📲 Incoming call screen with custom synthesised ringtone, accept/decline, and blind transfer (SIP 302 redirect to any extension/number).
  • 🎚️ In-call controls: mute, hold, attended transfer (REFER), add call / conference, and a DTMF keypad that sends tones over RTP (RFC 4733) with SIP INFO fallback.
  • 🔈 Teams-style audio device picker — choose speaker (output) and microphone (input) mid-call.
  • 🟡🟢🔴 Live registration status indicator (connecting / connected / disconnected).
  • ⚠️ Error toasts — busy, declined, no answer, connection lost, etc.
  • 🎨 Light & dark themes; FAB position is configurable.
  • 🖌️ Fully replaceable UI — swap the dialer, in-call, and incoming-call screens for your own React components via the components prop.
  • 🧩 Two usage tiers: a one-tag widget, or composable SipProvider + useSip() + individual components.

Installation

npm install sip-phone-lib

Peer dependencies (provide these in your app):

npm install react react-dom prop-types

React 16.7+, 17, 18, or 19 is supported. sip.js is bundled.


Quick start — the all-in-one widget

The fastest path: drop <SoftphoneWidget> anywhere in your app. It wraps its own SIP connection, renders a FAB (bottom-right by default), and handles every call state internally.

import { SoftphoneWidget } from 'sip-phone-lib';

function App()
{
	return (
		<SoftphoneWidget
			theme="dark"
			position="bottom-right"
			config={{
				displayName   : 'Jane Doe',
				token         : 'YOUR_AGENT_TOKEN',
				outboundProxy : 'wss://your-sip-proxy.example.com/ws',
				autoRegister  : true,
			}}
			onNotify={(n) => console.log('[sip]', n.type, n.text)}
		/>
	);
}

That's it — the agent registers on mount, the FAB appears, clicking it opens the dialer, and incoming calls pop the ringing screen automatically.

<SoftphoneWidget> props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | config | object | — (required) | SIP connection config — see Configuration. | | theme | 'light' \| 'dark' | 'dark' | UI theme. | | position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | FAB corner. | | style | object | — | Raw CSS offsets to override position, e.g. { bottom: 80, right: 40 }. | | onNotify | (n) => void | — | Callback for every notification ({ type, text }). | | components | { Dialer?, InCall?, IncomingCall? } | — | Replace any built-in screen with your own React component — see Custom UI. |

Click to call — trigger calls from your own buttons

Any element in your app (a contact card, a CRM row, a "Call support" button) can start a call programmatically. There are two ways, depending on how you embed the phone.

1. With <SoftphoneWidget> — imperative ref. The widget forwards a ref exposing call / open / close:

import { useRef } from 'react';
import { SoftphoneWidget } from 'sip-phone-lib';

function App()
{
	const phone = useRef(null);

	const contacts = [
		{ name : 'Sales',   number : '+9611234567' },
		{ name : 'Support', number : '2001'        },
	];

	return (
		<>
			{contacts.map((c) => (
				<button key={c.number} onClick={() => phone.current.call(c.number)}>
					📞 Call {c.name}
				</button>
			))}

			<button onClick={() => phone.current.open()}>Open dialer</button>

			<SoftphoneWidget ref={phone} config={{ /* … */ }} />
		</>
	);
}

| Method | Description | | --- | --- | | call(number) | Dial a number/extension and open the panel. | | open() | Open the dialer panel. | | close() | Close the dialer panel. |

2. Inside a <SipProvider> — the useSip() hook. If you use the composable setup (see Composable usage), any component in the tree can dial directly — no ref plumbing:

import { SipProvider, SoftphoneFab, useSip } from 'sip-phone-lib';

function CallButton({ number, children })
{
	const { call, registered } = useSip();

	return (
		<button disabled={!registered} onClick={() => call(number)}>
			{children}
		</button>
	);
}

export default function App()
{
	return (
		<SipProvider config={{ /* … */ }}>
			<CallButton number='+9611234567'>📞 Call sales</CallButton>

			<SoftphoneFab theme='dark' position='bottom-right' />
		</SipProvider>
	);
}

Custom UI — bring your own components

You are not locked into the built-in design. Every screen the widget renders — the dialer, the in-call card, and the incoming-call overlay — can be replaced with your own React component via the components prop. The widget keeps handling all SIP signalling, state, ringtones, the FAB, and screen switching; your components only render UI and call the handlers they're given.

import { useState } from 'react';
import { SoftphoneWidget } from 'sip-phone-lib';

function MyDialer({ registered, onCall })
{
	const [ number, setNumber ] = useState('');

	return (
		<div className="my-dialer">
			<span>{registered ? 'Online' : 'Offline'}</span>
			<input value={number} onChange={(e) => setNumber(e.target.value)} />
			<button onClick={() => onCall(number)}>Call</button>
		</div>
	);
}

function MyInCall({ session, onHangup, onMute, onSendDtmf })
{
	return (
		<div className="my-in-call">
			<h3>{session?.displayName}</h3>
			<button onClick={() => onMute(true)}>Mute</button>
			<button onClick={() => onSendDtmf('1')}>1</button>
			<button onClick={onHangup}>Hang up</button>
		</div>
	);
}

function MyIncomingCall({ contact, onAccept, onDecline, onForward })
{
	return (
		<div className="my-incoming">
			<h3>{contact.name} ({contact.number})</h3>
			<button onClick={onAccept}>Answer</button>
			<button onClick={onDecline}>Decline</button>
		</div>
	);
}

<SoftphoneWidget
	config={{ /* … */ }}
	components={{
		Dialer       : MyDialer,
		InCall       : MyInCall,
		IncomingCall : MyIncomingCall,
	}}
/>

Override any subset — screens you omit keep the built-in design.

Full example — a completely different design

A styled set of overrides that looks nothing like the built-in kit (gradient card, pill buttons, glass keypad), showing the typical wiring for each screen:

import { useState } from 'react';
import { SoftphoneWidget } from 'sip-phone-lib';

// ── Shared styles ──────────────────────────────────────────────
const card = {
	width : 300, padding : 20, borderRadius : 24,
	background : 'linear-gradient(160deg,#1e1b4b 0%,#4c1d95 100%)',
	color : '#ede9fe', display : 'flex', flexDirection : 'column', gap : 14,
	boxShadow : '0 16px 48px rgba(30,27,75,.55)', boxSizing : 'border-box',
};

const pill = (bg) => ({
	border : 'none', borderRadius : 999, padding : '12px 0', width : '100%',
	fontSize : 15, fontWeight : 600, color : '#fff', background : bg, cursor : 'pointer',
});

const key = {
	border : '1px solid rgba(255,255,255,.14)', borderRadius : 16,
	background : 'rgba(255,255,255,.08)', color : '#ede9fe',
	fontSize : 18, padding : '12px 0', cursor : 'pointer',
};

// ── Dialer ─────────────────────────────────────────────────────
function MyDialer({ registered, onCall, sessionHistory })
{
	const [ number, setNumber ] = useState('');

	return (
		<div style={card}>
			<div style={{ fontSize : 13 }}>
				{registered ? '🟢 Ready to call' : '🔴 Offline'}
			</div>

			<input
				value={number}
				onChange={(e) => setNumber(e.target.value)}
				placeholder='Enter a number…'
				style={{
					border : 'none', borderRadius : 14, padding : '13px 16px',
					fontSize : 20, background : 'rgba(0,0,0,.30)', color : '#fff',
				}}
			/>

			<div style={{ display : 'grid', gridTemplateColumns : 'repeat(3,1fr)', gap : 8 }}>
				{[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#' ].map((d) => (
					<button key={d} style={key} onClick={() => setNumber((n) => n + d)}>
						{d}
					</button>
				))}
			</div>

			<button
				style={pill('#7c3aed')}
				disabled={!registered || !number}
				onClick={() => onCall(number)}
			>
				📞 Call
			</button>
		</div>
	);
}

// ── In-call ────────────────────────────────────────────────────
function MyInCall({ session, onHangup, onHold, onUnhold, onMute, onSendDtmf })
{
	const [ muted, setMuted ] = useState(false);
	const [ held, setHeld ]   = useState(false);

	const toggleMute = () => { onMute(!muted); setMuted(!muted); };
	const toggleHold = () => { held ? onUnhold() : onHold(); setHeld(!held); };

	return (
		<div style={card}>
			<div style={{ textAlign : 'center' }}>
				<div style={{ fontSize : 18, fontWeight : 700 }}>
					{session?.displayName || 'In call'}
				</div>
				<div style={{ fontSize : 12, opacity : .7 }}>
					{held ? 'On hold' : 'Connected'}
				</div>
			</div>

			<div style={{ display : 'flex', gap : 8 }}>
				<button style={pill('rgba(255,255,255,.14)')} onClick={toggleMute}>
					{muted ? 'Unmute' : 'Mute'}
				</button>
				<button style={pill('rgba(255,255,255,.14)')} onClick={toggleHold}>
					{held ? 'Resume' : 'Hold'}
				</button>
			</div>

			<div style={{ display : 'grid', gridTemplateColumns : 'repeat(6,1fr)', gap : 6 }}>
				{[ '1', '2', '3', '4', '5', '6' ].map((d) => (
					<button key={d} style={{ ...key, padding : '8px 0', fontSize : 14 }}
						onClick={() => onSendDtmf(d)}>
						{d}
					</button>
				))}
			</div>

			<button style={pill('#ef4444')} onClick={onHangup}>
				End call
			</button>
		</div>
	);
}

// ── Incoming call (replaces the whole overlay + ringtone) ──────
function MyIncomingCall({ contact, onAccept, onDecline })
{
	return (
		<div style={{
			position : 'fixed', inset : 0, zIndex : 2147483000,
			background : 'rgba(15,10,40,.72)', backdropFilter : 'blur(6px)',
			display : 'flex', alignItems : 'center', justifyContent : 'center',
		}}>
			<div style={{ ...card, textAlign : 'center', width : 320 }}>
				<div style={{ fontSize : 13, letterSpacing : 2, opacity : .7 }}>
					INCOMING CALL
				</div>
				<div style={{ fontSize : 24, fontWeight : 800 }}>{contact.name}</div>
				<div style={{ fontSize : 14, opacity : .75 }}>{contact.number}</div>

				<div style={{ display : 'flex', gap : 10, marginTop : 6 }}>
					<button style={pill('#ef4444')} onClick={onDecline}>Decline</button>
					<button style={pill('#22c55e')} onClick={onAccept}>Answer</button>
				</div>
			</div>
		</div>
	);
}

// ── Wire everything up ─────────────────────────────────────────
export default function App()
{
	return (
		<SoftphoneWidget
			theme='dark'
			position='bottom-right'
			components={{
				Dialer       : MyDialer,
				InCall       : MyInCall,
				IncomingCall : MyIncomingCall,
			}}
			config={{ /* … */ }}
		/>
	);
}

Props your components receive

Every override receives theme ('light' | 'dark') and sip — the full useSip() context (all state + all actions), so nothing is out of reach. On top of that, each screen gets ready-made props:

Dialer

| Prop | Type | Description | | --- | --- | --- | | onCall | (number) => void | Dial a number/extension. | | registered | boolean | Registration status. | | registerInProgress | boolean | REGISTER in flight. | | displayName | string | The configured line label. | | sessionHistory | array | Recent calls (for a recents list). |

InCall

| Prop | Type | Description | | --- | --- | --- | | session | object | The active session wrapper (displayName, sipSession, sessionState, …). | | onHangup | () => void | Terminate the call. | | onHold / onUnhold | () => void | Hold / resume. | | onMute | (mute: boolean) => void | Mute / unmute the microphone. | | onSendDtmf | (tones: string) => void | Send DTMF digits. |

For transfer, add-call, or device switching, use the sip prop (sip.refer(...), sip.addCall(...), sip.getAudioDevices(), …).

IncomingCall

| Prop | Type | Description | | --- | --- | --- | | contact | { name, number } | Who is calling. | | isActive | boolean | A call is ringing. | | onAccept | () => void | Answer the call. | | onDecline | () => void | Reject the call. | | onForward | (uri) => void | Blind-transfer the ringing call. | | recents | array | Last 10 calls ({ displayName, number, time, direction }). | | lineName | string | The configured line label. | | session | object | The raw ringing session wrapper. |

Your IncomingCall replaces the whole overlay, including the built-in synthesised ringtone — play your own sound if you want one.

Prefer to own the entire layout (no FAB, no floating panel)? Skip the widget and compose SipProvider + useSip() directly — see Composable usage. The components prop is for keeping the widget's orchestration while swapping the visuals.


Configuration

The config object is passed to the underlying SIP client.

| Field | Type | Description | | --- | --- | --- | | outboundProxy | string | (required) WebSocket URL of your SIP proxy, e.g. wss://host/ws. | | displayName | string | The agent's display name / line label shown in the UI. | | token | string | Agent token sent as X-Agent-Token on every REGISTER. The proxy validates it, resolves the real SIP credentials, and answers the digest challenge — so the browser never holds the SIP password. | | password | string | SIP auth password (only if you authenticate directly instead of via token). | | autoRegister | boolean | Register automatically on mount. | | videoEnabled | boolean | Reserved for video (audio-only today). | | iceServers | array | Custom RTCIceServer[] for STUN/TURN. Falls back to window.iceServers. | | headers | { name, value }[] | Extra SIP headers added to outbound INVITEs (e.g. trunk / caller-id routing). |

Identity model: the browser sends a fixed placeholder AOR; the SIP proxy rewrites it to the real sipUsername@sipDomain (resolved from token) in both directions. Configure your proxy's placeholder user/host to match the library's.


Composable usage

For full control over layout, wrap your tree in SipProvider and use the useSip() hook and/or the individual components.

import {
	SipProvider, useSip,
	DialPad, InCall, IncomingCall, SoftphoneFab,
	sessionStates,
} from 'sip-phone-lib';

function Phone()
{
	const { currentSession, ringingSession } = useSip();
	const state = currentSession?.sessionState;

	return (
		<>
			{ringingSession && <IncomingCall />}
			{state === sessionStates.ACCEPTED && <InCall />}
			{!currentSession && !ringingSession && <DialPad />}
		</>
	);
}

export default function App()
{
	return (
		<SipProvider config={{ /* … */ }} onNotify={(n) => console.log(n)}>
			<Phone />
			{/* or: <SoftphoneFab theme="dark" position="bottom-right" /> */}
		</SipProvider>
	);
}

SoftphoneFab is the FAB/floating panel without its own provider — use it when you already render a SipProvider yourself. SoftphoneWidget = SipProvider

  • SoftphoneFab in one.

Exports

| Export | Description | | --- | --- | | SoftphoneWidget | All-in-one widget (provider + FAB + all screens). Default for embedding. | | SoftphoneFab | FAB + floating panel; expects a surrounding SipProvider. | | SipProvider | Context provider that owns the SIP connection. | | useSip() | Hook returning SIP state + actions (must be inside SipProvider). | | DialPad | Dialer card (keypad + collapsible recents). | | InCall | In-call card (controls, DTMF keypad, device picker, transfer/add-call). | | IncomingCall | Ringing screen (ringtone, accept/decline, blind transfer). Accepts component + theme props to render a custom UI with the same wiring. | | sessionStates | Symbols for session states (see below). |


The useSip() API

useSip() returns the SIP context: reactive state plus actions.

State

| Field | Type | Description | | --- | --- | --- | | displayName | string | Configured agent name. | | registered | boolean | Whether the agent is registered. | | registerInProgress | boolean | Registration in flight. | | registrationMessage | string | Last registration result/cause. | | currentSession | object \| null | Active call session wrapper. | | ringingSession | object \| null | Incoming, not-yet-answered session. | | sessions | object | Map of all sessions (for conference/add-call). | | sessionHistory | array | Recent calls ({ displayName, sipUri, direction, startTime }). | | lastNotification | { type, text, id } \| null | Most recent notification (errors surface as a toast in the widget). |

Actions

| Method | Description | | --- | --- | | register() / unRegister() | Manage SIP registration. | | call(number) / invite(uri) | Place an outbound call. | | accept(sipSession) | Answer an incoming call. | | terminate(sipSession) | Hang up / decline. | | hold(sipSession) / unhold(sipSession) | Hold / resume. | | addCall(uri) | Hold the current call and dial a second party (conference). | | redirect(sipSession, uri) | Blind-transfer an incoming (unanswered) call via SIP 302. | | refer(sipSession, uri) | Attended/blind transfer of an active call via REFER. | | sendDtmf(sipSession, tones) | Send DTMF over RTP (RFC 4733), INFO fallback. | | toggleMyMedia(session, type, mute) | Mute/unmute 'audio'/'video'. | | setSpeakerOutput(on) | Legacy speaker on/off toggle. | | getAudioDevices() | Promise<{ inputs, outputs }> of available mics/speakers. | | setAudioOutputDevice(deviceId) | Route call audio to a speaker. | | setAudioInputDevice(session, deviceId) | Switch microphone mid-call (replaceTrack). |

sessionStates

NEW, PROGRESS, REJECTED, IGNORED, ACCEPTED, CANCELED, FAILED, TERMINATED, INCOMING, OUTGOING, plus REFER-related states. They are JS Symbols — compare with ===:

import { sessionStates } from 'sip-phone-lib';

if (currentSession?.sessionState === sessionStates.ACCEPTED) { /* in call */ }

Notifications & call-failure messages

Every significant event emits a notification ({ type: 'success' | 'error' | 'info', text }). The widget shows error notifications as an auto-dismissing toast, and they're also stored in useSip().lastNotification. Call failures are classified into readable text — e.g. Busy, Declined, No answer, Unavailable, Server error (5xx), Connection lost. Pass onNotify to also log or surface them yourself:

<SoftphoneWidget
	config={{ /* … */ }}
	onNotify={(n) => { if (n.type === 'error') showBanner(n.text); }}
/>

Feature flags (optional)

A few in-call/dialer affordances can be hidden via globals set before render:

window.showDialpad        = false; // hide the dial pad
window.showHoldButton     = false; // hide Hold
window.showTransferButton = false; // hide Transfer
window.showAddCallButton  = false; // hide Add Call

Scripts

| Script | Description | | --- | --- | | npm run dev | Start the Vite dev app (src/main.jsx) for local testing. | | npm run build | Build the library to dist/ (ES + UMD). | | npm test | Run the test suite (Vitest). |

Build outputs: dist/sip-phone-lib.es.js (ESM) and dist/sip-phone-lib.umd.js (UMD), referenced by the package module / main fields.


Browser requirements

  • WebRTC + Web Audio (modern Chromium, Firefox, Safari).
  • A reachable WebSocket SIP proxy (wss://…) — required for registration and calls.
  • Output-device selection (setSinkId) is best-supported in Chromium; mic switching works broadly.

License

See repository for license details.