@callsync/webrtc-sip-sdk-react
v0.3.1
Published
TypeScript SDK for React/Next.js WebRTC/SIP Gateway integration
Maintainers
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,RTCPeerConnectionefetchnativos) - 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-reactAs 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
GatewayClientfora da árvore React — no nível de módulo ou viauseRef. Nunca dentro deuseStateou 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/vanillanão é mais o exemplo de referência e não receberá novos comportamentos. Para integrações React, useexamples/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 HEADEstrutura 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
