SDK JavaScript
@moraviq/voice-web: módulo ES sem dependência, com tipos TypeScript. Só áudio, com o WebRTC do navegador.
npm install @moraviq/voice-web # piloto: registro do GitHub Packages, escopo @moraviq
Configurar
import { VoiceClient } from '@moraviq/voice-web';
const voz = new VoiceClient({
baseUrl: 'https://api.voice.moraviq.com.br', // sandbox: o endereço do sandbox
// O seu backend devolve o token de aparelho da pessoa logada.
getToken: async () => {
const r = await fetch('/api/voz/token', { method: 'POST', cache: 'no-store' });
if (!r.ok) throw new Error('token');
return (await r.json()).token;
},
});
Portaria: entrar na chamada que o seu backend criou
// no clique de Ligar (o microfone é pedido aqui)
const { call_id } = await (await fetch('/api/portaria/chamar', { method: 'POST', body })).json();
const chamada = await voz.attachOutgoing(call_id, { audioElement: document.querySelector('audio') });
chamada.addEventListener('state', () => mostrar(chamada.status, chamada.answeredBy));
chamada.addEventListener('ended', (e) => fim(e.detail.status, e.detail.reason));
chamada.setMuted(true);
await chamada.hangup();
await chamada.setDisposition('allowed'); // informação para o seu sistema; não abre portão
Entre em até 10 s depois de criada, senão a chamada termina com origin_gone.
Morador: receber e atender
// Morador na web com o SDK JS (v2): registra o aparelho, fica no WSS e atende.
// ?usuario=res_77 (ext_id do morador no sistema do parceiro)
// ?auto=1 atende sozinho (robô dos testes); ?relay=1 força TURN.
// Escreve window.__voiceResult no fim, para o teste ponta a ponta.
import { VoiceClient } from '/sdk/web/moraviq-voice.js';
const q = new URLSearchParams(location.search);
const usuario = q.get('usuario') || 'res_77';
const auto = q.get('auto') === '1';
const relay = q.get('relay') === '1';
const $ = (id) => document.getElementById(id);
window.__voiceReady = false;
window.__voiceResult = null;
// installation_id: um por navegador. Fica no localStorage (não é segredo).
let inst = null;
try { inst = localStorage.getItem('voz.installation_id'); } catch (_) { /* aba privada */ }
if (!inst) {
inst = `web-${crypto.randomUUID()}`;
try { localStorage.setItem('voz.installation_id', inst); } catch (_) { /* segue sem guardar */ }
}
const config = await (await fetch('/api/config')).json();
const client = new VoiceClient({
baseUrl: config.baseUrl,
getToken: async () => {
const r = await fetch('/api/voz/token', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, cache: 'no-store',
body: JSON.stringify({ papel: 'morador', usuario, installation_id: inst }),
});
if (!r.ok) throw new Error('token');
return (await r.json()).token;
},
});
client.addEventListener('connectionStateChanged', (e) => { $('conexao').textContent = `conexão: ${e.detail.state}`; });
let atual = null;
let ultimas = null;
let medidor = null;
function fim(status, motivo) {
clearInterval(medidor);
$('tocando').hidden = true;
$('falando').hidden = true;
if (!window.__voiceResult) window.__voiceResult = { status, endReason: motivo, ...(ultimas || {}) };
}
async function atender() {
const c = atual;
try {
await c.accept({ audioElement: $('audio'), forceRelay: relay });
} catch (e) {
fim(`erro_${e.code || 'atender'}`);
return;
}
$('tocando').hidden = true;
$('falando').hidden = false;
// As métricas somem quando a conexão fecha: guarda a última a cada segundo.
medidor = setInterval(async () => {
if (c.pc) ultimas = await c.stats();
$('estado-chamada').textContent = `${c.status}${ultimas ? `, ${ultimas.selectedCandidateType || '?'}` : ''}`;
}, 1000);
}
client.addEventListener('incomingCall', (e) => {
atual = e.detail.incoming;
const posto = e.detail.call && e.detail.call.caller && e.detail.call.caller.station_label;
$('quem').textContent = posto || 'Portaria';
$('tocando').hidden = false;
atual.addEventListener('ended', async (ev) => fim(ev.detail.status, ev.detail.reason));
if (auto) atender();
});
$('atender').onclick = atender;
$('recusar').onclick = () => atual && atual.reject();
$('desligar').onclick = () => atual && atual.hangup();
await client.registerDevice({ installationId: inst, platform: 'web', pushType: 'none' });
await client.connect();
window.__voiceReady = true;Eventos do cliente
| Evento | detail |
|---|---|
incomingCall | {callId, call, incoming}; atenda com incoming.accept() |
callStateChanged | {callId, status, call, handle} |
callEnded | {callId, status, endReason, call, handle} |
takenElsewhere | {callId} |
connectionStateChanged | {state}: idle, connecting, open, reconnecting, closed |
error | {code}: mic_denied, token_failed, not_allowed, taken_elsewhere, attendant_busy, ... |
Cada chamada (OutgoingCall, IncomingCall) emite state, ended, media, ice, ice-restart, track, autoplay-blocked (mostre um botão que chame audio.play()) e error. Métricas: await chamada.stats() (tipo de candidato, RTT, jitter, perda).
Outros métodos
placeCall({site, target, context}): o aparelho cria a chamada (site em modocallback).registerDevice({installationId})eunregisterDevice();connect()edisconnect().getCall(id),listCalls({site, since, cursor, limit}),ringAck(id, result),cancel(id).VoiceClient.forCall({callId, deviceId, deviceSecret, baseUrl}): cliente de uma chamada só, pelo token curto da credencial do aparelho.forceRelay: trueemattachOutgoingouaccept: só TURN (teste de rede).
Cabeçalhos do seu site
Permissions-Policy: microphone=(self)nas páginas da portaria e do morador.- CSP, se usar:
connect-src 'self' https://api.voice.moraviq.com.br wss://api.voice.moraviq.com.br. - A origem do seu site precisa estar cadastrada no console (Apps e push, origens web): o Voice recusa CORS e WebSocket de outra origem.