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

@callsync/webrtc-sip-sdk-react

v0.3.1

Published

TypeScript SDK for React/Next.js WebRTC/SIP Gateway integration

Readme

@callsync/webrtc-sip-sdk-react

SDK TypeScript para integrar aplicações React / Next.js com o WebRTC/SIP Gateway — um servidor Go que faz a ponte entre clientes WebRTC no browser e uma central SIP (Asterisk).

Funcionalidades

  • Zero dependências de runtime (usa WebSocket, RTCPeerConnection e fetch nativos)
  • TypeScript estrito em todo o código
  • Reconexão automática com backoff exponencial e keep-alive via PING/PONG
  • Suporte a ICE trickle e priorização de Opus, com PCMU/PCMA como fallback no SDP
  • Hooks React 18 com useSyncExternalStore — sem Context, sem Provider, sem re-renders desnecessários
  • Seguro para SSR (Next.js App Router e Pages Router)

Instalação

Este pacote é publicado no GitHub Packages. Adicione o registry do escopo ao .npmrc do seu projeto (ou globalmente):

# .npmrc
@callsync:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Em seguida, instale:

npm install @callsync/webrtc-sip-sdk-react
# ou
pnpm add @callsync/webrtc-sip-sdk-react

As peer dependencies do React são opcionais — necessárias apenas se você usar o sub-caminho /react.

npm install react react-dom   # React 18+

Início Rápido

React / Next.js (App Router)

// lib/gateway.ts — crie UMA vez fora da árvore React
import { GatewayClient } from "@callsync/webrtc-sip-sdk-react";

export const gatewayClient = new GatewayClient({
  serverUrl:
    process.env.NEXT_PUBLIC_GATEWAY_URL ?? "wss://gateway.example.com/ws",
  // Call-center: trata todo OFFER inbound automaticamente.
  // Remova ou use false para exigir uma decisão manual por chamada.
  autoAnswer: true,
});
// app/page.tsx
"use client";
import { useEffect, useCallback } from "react";
import { useGateway, useCallEvents } from "@callsync/webrtc-sip-sdk-react/react";
import { gatewayClient } from "@/lib/gateway";

export default function Page() {
  const { connectionState, currentSessionId, login, call, hangup } =
    useGateway(gatewayClient);

  useEffect(() => {
    gatewayClient.connect();
    return () => {
      gatewayClient.disconnect();
    };
  }, []);

  useCallEvents(
    gatewayClient,
    useCallback((e) => {
      if (e.type === "call:incoming") {
        // Com autoAnswer: true, este evento é apenas uma notificação.
        console.log("Chamada recebida:", e.sessionId);
      }
      if (e.type === "call:connected") {
        console.log("Mídia conectada:", e.sessionId);
      }
      if (e.type === "call:ended") {
        console.log("Chamada encerrada:", e.reason);
      }
    }, [])
  );

  return (
    <div>
      <p>Status: {connectionState}</p>
      <button onClick={() => login("1001", "senha123")}>Login</button>
      <button onClick={() => call("9999")}>Ligar para 9999</button>
      {currentSessionId && (
        <button onClick={() => hangup(currentSessionId)}>Desligar</button>
      )}
    </div>
  );
}

Ciclo de vida e atendimento automático

Configure autoAnswer: true por instância para que o SDK responda chamadas inbound depois de preparar o SDP local. O padrão é false, preservando answer(sessionId) e reject(sessionId) no modo manual. Em autoanswer, a aplicação recebe call:incoming como notificação, mas não deve chamar answer() ou reject() para arbitrar a chamada; políticas condicionais por fila ou CRM ficam na aplicação antes de criar a instância.

Use eventos para efeitos pontuais e callState de useGateway() para renderização. Os marcos não são equivalentes: call:created significa que o Gateway criou a sessão outbound; call:ringing é SIP 180; call:answered significa que o SDP do SIP 200 chegou; call:answer-sent confirma a resposta inbound enviada; call:connected ocorre somente quando a RTCPeerConnection está conectada e é o marco operacional recomendado para CRM, timers e controles. call:ended é terminal e inclui rejeição local como reason: 'rejected'.

const client = new GatewayClient({
  serverUrl: "wss://gateway.example/ws",
  autoAnswer: true,
});

useCallEvents(client, (event) => {
  if (event.type === "call:connected") startOperationalTimer(event.sessionId);
  if (event.type === "call:ended") stopOperationalTimer(event.sessionId);
});

Atenção — React StrictMode: sempre crie o GatewayClient fora da árvore React — no nível de módulo ou via useRef. Nunca dentro de useState ou diretamente no corpo do componente. O StrictMode executa efeitos duas vezes e destruiria a conexão WebSocket na montagem.


Vanilla JS (sem framework) — legado

Legado/descontinuado: examples/vanilla não é mais o exemplo de referência e não receberá novos comportamentos. Para integrações React, use examples/nextjs, o exemplo oficial desta package. O diretório Vanilla só será removido após a migração dos consumidores estar documentada.

import { GatewayClient } from "@callsync/webrtc-sip-sdk-react";

const client = new GatewayClient({ serverUrl: "wss://gateway.example.com/ws" });

client.on("event", (e) => {
  if (e.type === "call:incoming") {
    console.log("Chamada recebida:", e.sessionId);
    client.answer(e.sessionId);
  }
});

await client.connect();
await client.login("1001", "senha123");
const sessionId = await client.call("9999");
// ... mais tarde
await client.hangup(sessionId);

Referência da API

new GatewayClient(config)

| Opção | Tipo | Padrão | Descrição | | ---------------------- | ------------------ | -------------- | ----------------------------------------------------------------------------------- | | serverUrl | string | — | URL do WebSocket, ex.: wss://host/ws | | pingInterval | number | 20000 | Intervalo do keep-alive em ms | | reconnectDelay | number | 3000 | Atraso base de reconexão em ms (dobra a cada tentativa, máximo 30 s) | | maxReconnectAttempts | number | 5 | Número máximo de tentativas de reconexão | | requestTimeout | number | 15000 | Prazo de LOGIN, OFFER, BYE e LOGOUT; expiração rejeita a operação | | iceGatheringTimeout | number | 2000 | Prazo do ICE gathering em offer/answer; preserva o SDP local disponível | | autoAnswer | boolean | false | Responde todo OFFER inbound automaticamente; call:incoming continua sendo emitido | | rtcConfiguration | RTCConfiguration | STUN do Google | ICE local; não cria relay/rota até o Gateway |

Métodos

| Método | Retorno | Descrição | | ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------- | | connect() | Promise<void> | Abre a conexão WebSocket | | disconnect() | Promise<void> | Fecha o WebSocket e libera todos os recursos | | login(userID, password) | Promise<{ expires: number }> | Autenticação SIP; userID exige ASCII imprimível sem acentos/espaços | | logout() | Promise<void> | Desregistro SIP; pode rejeitar por timeout/interrupção | | call(destination) | Promise<string> | Exige LOGIN_OK; resolve no SESSION_CREATED, antes de ringing/answer | | answer(sessionId) | Promise<void> | Atende uma chamada recebida; disponível somente com autoAnswer: false | | reject(sessionId) | Promise<void> | Rejeita uma chamada recebida com REJECT {}; disponível somente com autoAnswer: false | | hangup(sessionId) | Promise<void> | Encerra uma chamada ativa; resolve em BYE_OK ou rejeita por timeout/interrupção | | setMuted(muted) | void | Ativa/desativa o mudo do microfone local | | getAudioElement() | HTMLAudioElement \| null | Elemento de áudio interno para o áudio remoto | | on('event', handler) | void | Assina o stream de CallEvent | | off('event', handler) | void | Remove a assinatura do stream de CallEvent | | subscribeConnectionState(cb) | () => void | Assinatura para useSyncExternalStore | | getConnectionState() | ConnectionState | Snapshot síncrono do estado de conexão | | subscribeSession(cb) | () => void | Assinatura para useSyncExternalStore | | getSessionId() | string \| null | Snapshot síncrono do ID de sessão |

Propriedades (somente leitura)

| Propriedade | Tipo | Descrição | | ------------------ | --------------------- | --------------------------------- | | connectionState | ConnectionState | Estado atual da conexão WebSocket | | currentSessionId | string \| null | ID da sessão ativa, ou null | | localStream | MediaStream \| null | Stream do microfone local | | remoteStream | MediaStream \| null | Stream de áudio remoto |


useGateway(client)

Hook React que assina os dois slices de estado via useSyncExternalStore.

const {
  connectionState, // 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
  currentSessionId, // string | null
  login,
  logout,
  call,
  answer,
  reject,
  hangup,
} = useGateway(gatewayClient);

Todos os callbacks de ação são estáveis (useCallback com dep [client]) — seguros para passar como props sem causar re-renders extras.


useCallEvents(client, handler)

Assina o stream de eventos durante o ciclo de vida do componente. Remove a assinatura automaticamente no desmonte.

useCallEvents(
  client,
  useCallback((e) => {
    // trata CallEvent
  }, [])
);

O handler deve ser estabilizado com useCallback para evitar reinscrições desnecessárias a cada render.


Referência de Eventos

| Tipo do evento | Campos extras | Descrição | | ------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | connection:change | state: ConnectionState | Estado da conexão WebSocket alterado. | | login:success | expires: number | Login SIP bem-sucedido. | | login:failed | code: ErrorCode; message: string | Falha no login SIP. | | call:created | sessionId; direction: 'outbound'; destination | Gateway aceitou a oferta outbound e criou a sessão; ainda não é ringing, atendimento ou mídia. | | call:incoming | sessionId; sdp; from?; direction: 'inbound'; autoAnswer | Oferta inbound pronta localmente. Em autoanswer é uma notificação, não uma decisão manual. | | call:ringing | sessionId | SIP 180 para a chamada outbound. | | call:answered | sessionId; direction: 'outbound' | SDP do SIP 200 chegou; a mídia ainda pode não estar conectada. | | call:answer-sent | sessionId; direction: 'inbound'; autoAnswer | SDK enviou a resposta SDP inbound ao Gateway. | | call:connected | sessionId; direction | RTCPeerConnection conectada; marco operacional recomendado para áudio, CRM e timer. | | call:ended | sessionId; reason; sipCode?; detail? | Término único da chamada; falha SIP inclui código/detalhe quando disponíveis. | | call:error | sessionId; code; message | Erro local ou de sinalização da chamada. |

CallState

getCallState() e subscribeCallState() expõem o snapshot estável { phase, sessionId, direction, destination?, from?, autoAnswer }. Em React, useGateway(client).callState é a fonte de render; eventos continuam próprios para efeitos pontuais.

ConnectionState

type ConnectionState =
  | "connecting"
  | "connected"
  | "disconnected"
  | "reconnecting";

CallEndReason

type CallEndReason =
  | "normal"
  | "cancelled"
  | "rejected"
  | "failed"
  | "connection-lost"
  | "media-failed";

reason é obrigatório em call:ended. Quando a origem é uma falha SIP, reason é "failed" e o evento inclui sipCode e detail quando disponibilizados pelo Gateway. Quando ICE ou a conexão WebRTC falha, reason é "media-failed"; o SDK encerra localmente e tenta BYE {} apenas se o WebSocket estiver conectado.

ErrorCode

type ErrorCode =
  | "AUTH_FAILED" // Falha de autenticação ou throttle
  | "UNAUTHORIZED" // OFFER sem LOGIN
  | "INTERNAL_ERROR"; // Erro de protocolo/ANSWER
// Ocupado e recusado chegam como BYE com sipCode.

Limites do browser e ICE

O browser envia Origin no upgrade WebSocket. Se o Gateway configurar server.http.allowed_origins, inclua a origem da aplicação; caso contrário o upgrade recebe HTTP 403.

Configurar STUN/TURN apenas no cliente não cria relay no Gateway. Sem relay/rota direta, browser e Gateway precisam de conectividade IP direta (LAN, VPN ou IP público configurado).

call() resolve quando o Gateway envia SESSION_CREATED: o sessionId já existe, mas a chamada ainda pode não estar tocando ou atendida. Acompanhe call:ringing, call:answered e call:ended. Queda do WebSocket e falha de mídia são terminais: o SDK não recupera a sessão anterior nem chama restartIce(), porque o Gateway não expõe renegociação SDP.

As operações remotas rejeitam por timeout (requestTimeout) ou interrupção de conexão; respostas normativas tardias não reabrem a operação. logout() antes de um LOGIN confirmado é local e não envia LOGOUT, pois o Gateway o trata como no-op silencioso.

O userID de login() deve usar somente ASCII imprimível, sem espaços ou acentos (por exemplo, usuario.teste, não usuário.teste). Isso evita codificação percentual no URI SIP observada no SBC.


Build

npm install
npm run build      # gera dist/index.{js,mjs,d.ts} e dist/react.{js,mjs,d.ts}
npm test           # testes unitários com Vitest
npm run typecheck              # tsc --noEmit
npm run test:coverage:changed  # ≥90% de linhas e branches em src/ modificado desde HEAD

Estrutura do Projeto

src/
├── index.ts                   # Barrel — exports principais
├── protocol/
│   ├── types.ts               # Enum MessageType, ErrorCode, SignalMessage
│   └── payloads.ts            # Interfaces de payload por tipo de mensagem
├── core/
│   ├── GatewayClient.ts       # Classe principal (sem dependência de framework)
│   ├── WebSocketTransport.ts  # Reconexão e keep-alive
│   └── WebRTCSession.ts       # Ciclo de vida do RTCPeerConnection + preferPCMU
├── react/
│   ├── useGateway.ts          # Hook useSyncExternalStore
│   ├── useCallEvents.ts       # Hook de assinatura de eventos
│   └── index.ts               # Exports do sub-caminho /react
└── types/
    └── events.ts              # Union type CallEvent, ConnectionState

examples/
├── nextjs/                    # Exemplo oficial: integração Next.js 14 App Router
└── vanilla/                   # Legado/descontinuado; remover após migração documentada dos consumidores

test/
├── protocol.test.ts           # Serialização de mensagens
├── sdp.test.ts                # Manipulação de SDP com preferPCMU
├── gateway-client.test.ts     # Máquina de estados do GatewayClient (WebSocket mockado)
└── hooks.test.tsx             # useGateway + useCallEvents (cliente mockado)

Licença

MIT